The pack
versusGarbage
function versusGarbage(): Pack<{ versusGarbage: typeof api }>
Install the versus garbage API.
Helpers
chainPotential
function chainPotential(f: Field, colors: readonly number[], pop: number, minY = 0): number
Longest chain that dropping one or two puyos of one colour onto one column would set off (the board's chain potential), by simulating pops and gravity on a copy.
CPU_LEVELS
const CPU_LEVELS: Readonly<Record<CpuLevel, CpuProfile>>
CPU levels for faller boards (Puyo: fire is the chain length it builds to).
cpuProfile
function cpuProfile(level: CpuLevel | (Partial<CpuProfile> & { level?: CpuLevel }), table: Readonly<Record<CpuLevel, CpuProfile>> = CPU_LEVELS): CpuProfile
A profile from a level name or overrides on one.
CRASH_CPU_LEVELS
const CRASH_CPU_LEVELS: Readonly<Record<CpuLevel, CpuProfile>>
CPU levels for crash-gem boards (fire is the gems a break must take unless it is under attack).
crashGemBoard
function crashGemBoard(game: CrashGemGame): CrashGemVersusBoard
Wraps a crash-gem board for a versus match.
crashGemCpu
function crashGemCpu(level: CpuLevel | (Partial<CpuProfile> & { level?: CpuLevel }) = 'normal'): CpuFactory<CrashGemVersusBoard, FallerInput>
A CPU for crash-gem boards: it builds same-coloured clusters and power gems, keeps the spawn column low, holds its
crash gems until a break would take fire gems, and breaks at once to counter incoming gems or when in danger.
createVersusMatch
function createVersusMatch<B extends VersusBoard<I>, I>(options: VersusOptions<B, I>): VersusMatch<B, I>
Creates a match. Throws a RangeError for an unsupported configuration.
DROP_CPU_LEVELS
const DROP_CPU_LEVELS: Readonly<Record<CpuLevel, CpuProfile>>
CPU levels for grab-and-throw boards: think frames between moves, moveEvery frames per column step.
dropBoard
function dropBoard(game: DropGame, opts: DropBoardOptions = {}): DropVersusBoard
Wraps a grab-and-throw game for a versus match.
dropCpu
function dropCpu(level: CpuLevel | (Partial<CpuProfile> & { level?: CpuLevel }) = 'normal'): CpuFactory<DropVersusBoard, DropInput>
A CPU for grab-and-throw boards (Magical Drop): for every grab column and throw column it tries the move on a snapshot, settles it, and scores lines sent, pieces cleared and the tallest column; then it walks the clown there and back at its level's speed. A failed grab re-plans.
fallerBoard
function fallerBoard(game: FallerGame, opts: FallerBoardOptions = {}): FallerVersusBoard
Wraps a faller game for a versus match.
fallerCpu
function fallerCpu(level: CpuLevel | (Partial<CpuProfile> & { level?: CpuLevel }) = 'normal'): CpuFactory<FallerVersusBoard, FallerInput>
A CPU for faller boards. Puyo rulesets: it values chain potential (the longest chain one or two more puyos would set
off), connected colours, low stacks and a clear third column, and fires when its chain reaches fire or when
garbage is on its way or the stack is high (so it counters). Rows rulesets: the classic height / lines / holes /
bumpiness weights plus attack. Others: points and attack against height.
magicalDropVersus
function magicalDropVersus(o: MagicalDropVersusOptions = {}): VersusOptions<DropVersusBoard, DropInput>
Magical Drop versus on grab-throw-columns (Data East): each board's chain sends its attack lines at chain end,
offsetting cancels waiting lines, and lines enter only while the receiving board is idle (the pack's rule). Proves
the versus layer is not tied to falling pieces.
marginAt
function marginAt(rule: MarginRule, seconds: number): { targetPoints: number | null; scale: number; step: number }
The margin state at seconds into a round: Tsu target points (or null) and the attack multiplier for boards
that cannot take target points.
puyoTsuVersus
function puyoTsuVersus(o: PuyoVersusOptions = {}): VersusOptions<FallerVersusBoard, FallerInput>
Puyo Puyo Tsu versus (Compile, 1994): two 6 x 12 wells dealt the same pairs from one seed, nuisance at 70 target points with the fraction carried, offsetting, nuisance held until the attacker's chain ends and then dropped after the receiver's next move (at most 30 a move), +30 after an all clear, margin time from 96 s, first to two wins. The fall speed stays at the first level (Tsu's versus has no speed levels; an assumption, see the docs).
PUZZLE_FIGHTER_MARGIN
const PUZZLE_FIGHTER_MARGIN: MarginRule
Super Puzzle Fighter II Turbo's attack multiplier by time, as set in the X' rebalance patch's data table (github.com/KScl/puzzle-fighter-x-prime): x1.1 from 75 s, then +0.1 every 30 s up to x1.7. The patch notes say it shortened the original's thresholds, which are not published, so this is the nearest documented table.
puzzleFighterVersus
function puzzleFighterVersus(o: PuzzleFighterVersusOptions = {}): VersusOptions<CrashGemVersusBoard, FallerInput>
Super Puzzle Fighter II Turbo versus (Capcom, 1996): two crash-gem boards on one drop order, counter gems sent at the end of each break in the attacker's drop pattern, offsetting (counter-attacks cancel waiting gems, survivors start at 3), counter gems falling after the receiver's next pair, the margin multiplier, first to two wins.
STANDARD_TRAY
const STANDARD_TRAY: TrayRule
The standard tray from Puyo Puyo! 15th Anniversary on: small 1, large 6, rock 30, star 180, moon 360, crown 720, comet 1440, at most six symbols (Puyo Nexus, "Nuisance queue").
targetPointsSequence
function targetPointsSequence(rule: Extract<MarginRule, { kind: 'targetPoints' }>): number[]
The target-points sequence of a 'targetPoints' rule, from the base value to the last reduction.
tetrisVersus
function tetrisVersus(o: TetrisVersusOptions = {}): VersusOptions<FallerVersusBoard, FallerInput>
Two-player Guideline Tetris: endless Marathon wells, line clears attack by the Guideline attack table (T-spins, back-to-back and combos included), waiting garbage cancelled by your own clears, garbage rising after a piece that clears nothing.
trayIcons
function trayIcons(amount: number, rule: TrayRule = TSU_TRAY): TrayIcon[]
The symbols for amount garbage, largest first, at most rule.max (greedy, as the games draw them).
TSU_MARGIN
const TSU_MARGIN: MarginRule
Puyo Puyo Tsu: 70 target points, margin time 96 s, a reduction every 16 s, at most 14 (70, 52, 35, 26, 17, 13, 8, 6, 4, 3, 2, 1).
TSU_TRAY
const TSU_TRAY: TrayRule
Puyo Puyo Tsu's tray: small 1, large 6 (a row), rock 30 (one drop), mushroom 200, star 300, crown 420 (Puyo Nexus, "Nuisance queue"; the Mega Drive version is presumed to show up to nine symbols).
warningLevel
function warningLevel(amount: number, thresholds: readonly [number, number, number] = [1, 11, 31]): '' | 'CAUTION' | 'WARNING' | 'DANGER'
Super Puzzle Fighter II Turbo's incoming warnings (PlayStation manual): CAUTION 1-10, WARNING 11-30, DANGER 31+.
Classes
CrashGemVersusBoard
class CrashGemVersusBoard implements VersusBoard<FallerInput>
A crash-gem board (faller's CrashGemGame, Puzzle Fighter rules) in a versus match. Each break attacks with its
counter gems (the running total for the move, after the board's own send rate); receive queues counter gems in the
ATTACKER's drop pattern; offset cancels them (the survivors then start at the shorter countdown).
readonly kind: "crash-gems"— Board family.readonly game: CrashGemGame— The game.constructor(game: CrashGemGame)— Wraps a board.get pattern(): readonly string[]— This board's drop pattern (what its attacks look like on the other side).step(input: FallerInput): void— One 60 Hz frame.take(): VersusBoardEvent[]— Normalised events since the last call.events(): CrashGemEvent[]— The board's own events since the last call.receive(amount: number, from: VersusBoard<never> | null): void— Queues counter gems in the attacker's pattern.offset(amount: number): number— Cancels waiting counter gems.get pending(): number— Counter gems waiting.get lost(): boolean— Topped out.get score(): number— Score.snapshot(): CrashGemSnapshot— Snapshot.restore(s: unknown): void— Restore.
DropVersusBoard
class DropVersusBoard implements VersusBoard<DropInput>
A grab-throw-columns game (Magical Drop) in a versus match. Its attack event (lines for the chain, at chain end)
attacks; receive queues lines that enter from the ceiling only while this board is idle (the pack's rule);
offset cancels waiting lines. Lines are coloured by the attacker's pattern when it has one.
readonly kind: "drop"— Board family.readonly game: DropGame— The game.readonly pattern?: readonly string[]— Attack colour pattern, if any.constructor(game: DropGame, opts: DropBoardOptions = {})— Wraps a game.step(input: DropInput): void— Applies the input's actions, then advances 1/60 s.take(): VersusBoardEvent[]— Normalised events since the last call.events(): DropEvent[]— The game's own events since the last call.receive(amount: number, from: VersusBoard<never> | null): void— Queues lines, coloured by the attacker's pattern when it has one.offset(amount: number): number— Cancels waiting lines.get pending(): number— Lines waiting (kept as lines accepted byreceive, less those offset or landed, so no state copy is made each tick and the count is right the momentreceivereturns).get lost(): boolean— Lost.get score(): number— Score.snapshot(): { game: string; sentLines: number; waiting: number; total: number; dead: boolean }— Snapshot.restore(s: unknown): void— Restore.
FallerVersusBoard
class FallerVersusBoard implements VersusBoard<FallerInput>
A faller game in a versus match. Puyo-style rulesets (with rules.nuisance): chain steps attack with their
nuisance, receive queues nuisance (it falls after this player's next move, Tsu's rules), offset cancels it, and
margin time sets the target points. Rows rulesets (Tetris): line clears attack with rules.attack, receive
queues garbage rows that rise after the next piece that clears nothing, and offset cancels waiting rows. Other
rulesets (Columns, Dr. Mario) have no incoming-garbage hook in faller and are refused.
readonly kind: "faller"— Board family.readonly game: FallerGame— The game.readonly pattern?: readonly string[]— Attack colour pattern, if any.constructor(game: FallerGame, opts: FallerBoardOptions = {})— Wraps a game. Throws a RangeError for a ruleset with no incoming-garbage hook.step(input: FallerInput): void— One 60 Hz frame.take(): VersusBoardEvent[]— Normalised events since the last call.events(): FallerEvent[]— The game's own events since the last call (for sounds, toasts and animation).receive(amount: number, _from: VersusBoard<never> | null): void— Queues nuisance or garbage rows.offset(amount: number): number— Cancels waiting nuisance or garbage rows (newest rows first).get pending(): number— Nuisance or rows waiting.get lost(): boolean— Topped out.get score(): number— Score.setTargetPoints(points: number): void— Margin time (nuisance rulesets only).snapshot(): { game: FallerSnapshot; rowsWaiting: number }— Snapshot.restore(s: unknown): void— Restore.
VersusMatch
class VersusMatch<B extends VersusBoard<I>, I>
A versus match between puzzle boards. Call step(inputs) once per 60 Hz tick.
readonly options: VersusOptions<B, I>— The options in force.constructor(options: VersusOptions<B, I>)— Creates a match and its first round.get seats(): VersusSeat<B>[]— Seats, in order.board(i: number): B— Seati's board.incoming(i: number): number— Garbage on its way to seati(committed plus in transit).get round(): number— Round number (1-based).get phase(): VersusPhase— Phase.get winner(): number | null— The last round's winner (null for a draw), or the match winner once it is over.get ticks(): number— Ticks since the round started.get seconds(): number— Seconds since the round started.get margin(): { targetPoints: number | null; scale: number; step: number }— The margin state in force: target points (Tsu) and the attack multiplier for other boards.get alive(): number[]— Live seats this round.drain(): VersusEvent[]— Removes and returns the pending events.nextRound(): void— Starts the next round now (after a round is over and the match is not).restart(): void— Starts the whole match again (wins cleared, same options).step(inputs: readonly (I | undefined)[] = []): void— One 60 Hz tick: every board steps (CPU seats take their own input), attacks are routed, margin time advances, and the round is decided.inputs[i]is seat i's input (ignored for CPU seats).snapshot(): VersusSnapshot— A deep copy of the whole match (boards, transit, wins, CPU state).restore(snap: VersusSnapshot): void— Restores asnapshot()of a match with the same options.
Types
CpuFactory
type CpuFactory<B, I> = (board: B, seat: number, seed: number) => VersusCpu<I>
Builds a CPU for a board (called at the start of every round).
CpuLevel
type CpuLevel = 'easy' | 'normal' | 'hard'
CPU difficulty names.
CpuProfile
interface CpuProfile
How a CPU plays.
think: number— Frames it waits on a new piece before moving (its reaction time).moveEvery: number— Frames between its taps (shift, rotate).drop: 'none' | 'soft' | 'hard'— How it finishes a placement: let gravity drop it, hold soft drop, or hard drop where the rules allow.depth: 1 | 2— Look-ahead: 1 = the current piece; 2 = also the next piece for the bestbeamplacements.beam: number— Placements carried into the second ply, and the pool a mistake is picked from.mistakes: number— Chance of playing a random placement from the bestbeaminstead of the best.fire: number— Patience: the smallest attack it sets off unprompted (Puyo: chain length; crash gems: gems broken).budget: number— Previews it runs per tick while thinking.
CpuView
interface CpuView
What a CPU controller may know about the match beyond its own board.
incoming: number— Garbage on its way to this seat (committed plus held in transit).seconds: number— Seconds since the round started.outgoing: number— The highest attack waiting for any opponent (to judge whether to counter).
DropBoardOptions
interface DropBoardOptions
Options for dropBoard.
pattern?: readonly string[]— Colour pattern of this board's attacks: incoming lines take these colours (a character's attack pattern); without one, lines are random.
DropInput
interface DropInput
One tick of input for a grab-and-throw board: actions applied before the tick.
move?: -1 | 1— Step the clown one column.moveTo?: number— Jump straight to a column.grab?: boolean— Grab.throw?: boolean— Throw.push?: boolean— Push a row in (when the rules allow).
FallerBoardOptions
interface FallerBoardOptions
Options for fallerBoard.
pattern?: readonly string[]— Colour pattern of this board's attacks (for boards that colour incoming garbage).
MagicalDropVersusOptions
interface MagicalDropVersusOptions extends VersusPresetOptions
Options for magicalDropVersus.
patterns?: readonly (readonly string[] | null)[]— Attack colour pattern per seat (rows of R G B Y). Default: random lines.
MarginRule
type MarginRule = | { kind: 'none' } | { kind: 'targetPoints'; base: number; start: number; every: number; first: number; steps: number } | { kind: 'scale'; steps: readonly { at: number; scale: number }[] }
How attack power grows with time.
- 'none': never.
- 'targetPoints': Puyo Puyo Tsu. After
startseconds the target points (score per nuisance) fall, then again everyeveryseconds: the first reduction multiplies byfirst(0.75), each later one halves the value from two reductions before, rounded down; at moststepsreductions, never below 1 (Puyo Nexus, "Margin time"). Boards withsetTargetPointsget the value; others have their attacks scaled by base / current. - 'scale': attacks multiplied by
scalefromatseconds (Super Puzzle Fighter's margin table).
PuyoVersusOptions
interface PuyoVersusOptions extends VersusPresetOptions
Options for puyoTsuVersus.
colors?: number— Colours (Tsu: 3 Easy, 4 Normal, 5 Hard).popSize?: number— Pop size. Default 4.cols?: number— Well width. Default 6.rows?: number— Well height. Default 12.
PuzzleFighterVersusOptions
interface PuzzleFighterVersusOptions extends VersusPresetOptions
Options for puzzleFighterVersus.
patterns?: readonly (string | DropPattern)[]— Drop pattern per seat (keys of faller's DROP_PATTERNS, or your own). Default 'bands' and 'halves'.speeds?: readonly number[]— Drop speed per seat, 1..5 (the original's VS handicap). Default 3.diamond?: number— Diamond damage: 0.5 original, 0.8 HD Remix.colors?: number— Gem colours (2..4). Default 4.cols?: number— Board width. Default 6.rows?: number— Board height. Default 12.
TetrisVersusOptions
interface TetrisVersusOptions extends VersusPresetOptions
Options for tetrisVersus.
samePieces?: boolean— Both players get the same piece order. Default false (a bag each).
Transit
interface Transit
Garbage on its way to a seat, held while the attacker's move resolves.
from: number— Attacking seat.amount: number— Amount.
TrayIcon
interface TrayIcon
One tray symbol and how much garbage it stands for.
name: string— Symbol name ('small', 'large', 'rock', ...).value: number— Garbage it stands for.
TrayRule
interface TrayRule
A tray: its symbols (any order) and how many fit.
icons: readonly TrayIcon[]— Symbols.max: number— Most symbols shown at once.
VersusBoard
interface VersusBoard<I>
A puzzle board taking part in a versus match. One step is one 60 Hz tick. Adapters for the engine's board packs:
fallerBoard, crashGemBoard, dropBoard. Implement it for any other board that can attack and take garbage.
readonly kind: string— Board family, for reports ('faller', 'crash-gems', 'drop', ...).step(input: I): void— Advances the board one 60 Hz tick with this input.take(): VersusBoardEvent[]— Removes and returns the normalised events produced since the last call.receive(amount: number, from: VersusBoard<never> | null): void— Commits incoming garbage: it lands by the board's own rules (Puyo: after this player's next move).fromis the attacking board, whosepatternsome boards use to colour what lands.offset(amount: number): number— Cancels up toamountcommitted garbage that has not landed yet. Returns how much was cancelled.readonly pending: number— Committed garbage that has not landed yet.readonly lost: boolean— True once the board has topped out.readonly score: number— Score (used by the 'leader' routing).readonly pattern?: readonly string[]— The colours this board's attacks arrive in (rows of colour letters, top to bottom), if it has a pattern.setTargetPoints?(points: number): void— Sets Tsu-style target points (margin time). Boards without it get their attacks scaled instead.snapshot(): unknown— A deep copy of the board (plain data).restore(snapshot: unknown): void— Restores asnapshot()of a board with the same rules.
VersusBoardEvent
type VersusBoardEvent = /** One step of a move produced attack (a chain step, a line clear). Raw: before offsetting, handicaps, margin. */ | { type: 'attack'; amount: number; chain: number } /** The move that attacked has finished resolving: attacks held while it ran are released. */ | { type: 'moveEnd' } /** Garbage this board had received has landed on it. */ | { type: 'landed'; amount: number } /** The board emptied itself (Puyo all clear). */ | { type: 'allClear' } /** The board topped out. */ | { type: 'lost'; reason: string }
What a board reports to the versus layer, normalised from its own events.
VersusCpu
interface VersusCpu<I>
A CPU player: produces one input per tick for its own board.
input(view: CpuView): I— The input for this tick (called once per tick, before the board steps).snapshot(): unknown— Plain-data state for match snapshots.restore(snapshot: unknown): void— Restoressnapshot().
VersusEvent
type VersusEvent = | { type: 'round'; round: number } | { type: 'attack'; from: number; raw: number; amount: number; offset: number; to: number[] } | { type: 'commit'; from: number; to: number; amount: number } | { type: 'landed'; seat: number; amount: number } | { type: 'allClear'; seat: number } | { type: 'margin'; step: number; targetPoints: number | null; scale: number } | { type: 'lost'; seat: number; reason: string } | { type: 'roundOver'; round: number; winner: number | null } | { type: 'matchOver'; winner: number }
Events from drain().
VersusHandicap
interface VersusHandicap
Per-seat handicap.
send?: number— Multiplier on attacks this seat sends. Default 1.receive?: number— Multiplier on attacks this seat receives. Default 1.start?: number— Garbage committed to this seat when each round starts (Tsu CPU "Very Hard" starts with two rows). Default 0.
VersusOptions
interface VersusOptions<B extends VersusBoard<I>, I>
How a match is set up. Presets: puyoTsuVersus, puzzleFighterVersus, magicalDropVersus, tetrisVersus.
players: number— Seats, 2..8.board: (seat: number, round: number, seed: number) => B— Builds seatseat's board for a round.seedis the round's seed (the same for every seat whensharedSeed).seed?: number— Match seed. Default 1.sharedSeed?: boolean— Every board in a round gets the same seed, so the same pieces (Puyo Tsu, Puzzle Fighter). Default true.firstTo?: number— Round wins needed to take the match (best of 2n - 1). Default 2.routing?: VersusRouting— Attack routing. Default 'opponent'.offset?: boolean— A seat's attack first cancels garbage waiting for it (Tsu's offsetting, Puzzle Fighter's counter-attack). Default true.hold?: boolean— Attacks wait in transit until the attacker's move has finished (Tsu: nuisance cannot fall while the opponent's chain runs). Default true.margin?: MarginRule— Margin time. Default none.handicaps?: readonly VersusHandicap[]— Per-seat handicaps.cpu?: readonly (CpuFactory<B, I> | null)[]— Per-seat CPU players (null = human).draw?: 'replay' | 'share'— Both (all) remaining boards topping out on one tick: 'replay' the round with no point, or 'share' a point each. Default 'replay'.roundPause?: number— Ticks between a round ending and the next starting (whenautoNextRound). Default 150.autoNextRound?: boolean— Start the next round by itself afterroundPause. Default true; otherwise callnextRound().
VersusPhase
type VersusPhase = 'playing' | 'roundOver' | 'matchOver'
Match phase.
VersusPresetOptions
interface VersusPresetOptions
Options shared by the presets.
difficulty?: FallerDifficulty— Board difficulty (the board pack's Easy / Normal / Arcade). Default 'normal'.seed?: number— Match seed.players?: number— Seats. Default 2.firstTo?: number— Round wins to take the match. Default 2.cpu?: readonly (CpuLevel | null)[]— CPU level per seat (null = human). Default: seat 0 human, the others 'normal'.margin?: boolean— Margin time on. Default true.offset?: boolean— Offsetting on. Default true.handicaps?: readonly VersusHandicap[]— Per-seat handicaps.
VersusRouting
type VersusRouting = 'opponent' | 'all' | 'split' | 'leader' | 'random' | ((attacker: number, alive: readonly number[]) => number[])
Where a seat's attacks go. 'opponent': the other seat (two players) or the next live seat. 'all': every live opponent gets the full attack (Puyo four-player). 'split': shared out between them. 'leader': the live opponent with the highest score. 'random': a live opponent chosen by the match's seeded generator. Or a function.
VersusSeat
interface VersusSeat<B>
A seat in the match.
readonly index: number— Seat index.readonly board: B— This round's board.readonly wins: number— Rounds won.readonly incoming: number— Garbage on its way: committed to the board but not landed, plus held in transit.readonly transit: readonly Transit[]— The part ofincomingstill held while attackers' moves run.readonly lost: boolean— Lost this round.readonly cpu: boolean— True when a CPU plays this seat.readonly sent: number— Total attack sent this round (after offsetting).
VersusSnapshot
interface VersusSnapshot
A match snapshot (plain data plus each board's own snapshot).
round: number— Round number.phase: VersusPhase— Phase.tick: number— Ticks this round.pause: number— Pause ticks left after a round.winner: number | null— Winner of the last round or the match.marginStep: number— Margin step in force.rng: RngState— Where the match's seeded stream stands (a kernelRngsnapshot).seats: SeatState[]— Seats.boards: unknown[]— Each board's snapshot.


