Skip to content
Umpire / Live Board Stadium Night Game
Foul

starting_pitcher

Morrison pitched yesterday — scratched from tonight's lineup.

Status disabled
Reason needs rest
Reset to Flores

Help your app
play by the rules.

Declare fields. Declare rules. Umpire tells you what's in play — and calls foul when values are out of bounds. Forms, game boards, config panels, anything with interdependent state.

Tonight’s card is set — full roster against a right-handed pitcher. Now break it. Toggle Morrison to fatigued and watch him get pulled from the mound. Flip the opposing pitcher to a lefty and the platoon swaps kick in. Hit “Random injury” a few times and see slots empty out as players land on the IL.

RosterBoston Crabs
Jim SummerCFR/Rin lineup
Manny Delgado1BL/Lin lineup
Ricky Vega1B/DHR/Rplatoon matchup
Tommy NakamuraSSS/Rin lineup
Dave Kowalski3BR/Rin lineup
Carlos ReyesLFL/Lin lineup
Mike PattersonLF/RFR/Rplatoon matchup
Billy ChenCR/Rin lineup
Tony Russo2BL/Rin lineup
Andre WilliamsRF/CFR/Rin lineup
Marco SilvaDH/LFL/Lin lineup
Chris Hartley1B/DHS/R
Jake MorrisonSPR/Rin lineup
Eddie FloresSPL/L
Sam WhitfieldRP/CLR/R
Tonight's Lineupvs RHP
SPSPJake MorrisonR/R
1CBilly ChenR/R
22BTony RussoL/R
3SSTommy NakamuraS/R
41BManny DelgadoL/L
53BDave KowalskiR/R
6LFCarlos ReyesL/L
7CFJim SummerR/R
8RFAndre WilliamsR/R
9DHMarco SilvaL/L

Three rule types drive the whole card: enabledWhen (injuries and fatigue scratch players), oneOf (platoon matchups swap batters when the pitcher flips), and requires (Morrison needs rest before he can start). Every change produces fouls — fields that just lost their player and need a replacement. Clear the card and rebuild from scratch if you want to see it from the other direction.

The lineup card is a React component backed by a real @umpire/core instance. Here’s how the state flows:

Fields and conditions — declared once
// One field per player. Everything that affects eligibility is a condition.
type Conditions = {
opposingPitcher: 'L' | 'R'
injuries: Record<string, boolean>
morrisonRested: boolean
}
const lineupUmp = umpire<typeof fields, Conditions>({
fields,
rules: [
// oneOf: only one branch is active at a time.
// Delgado (L) starts vs righties, Vega (R) starts vs lefties.
oneOf('firstBasePlatoon', {
vsRighty: ['delgado'],
vsLefty: ['vega'],
}, {
activeBranch: (_v, c) => c.opposingPitcher === 'L' ? 'vsLefty' : 'vsRighty',
reason: 'platoon matchup',
}),
// enabledWhen: disabled when the predicate returns false.
...playerIds.map(id =>
enabledWhen(id, (_v, c) => !c.injuries[id], {
reason: 'on the injured list',
})
),
// Morrison can't pitch without rest.
enabledWhen('morrison', (_v, c) => c.morrisonRested, {
reason: 'needs rest',
}),
],
})
In the component — useUmpire does the rest
// Values: only players assigned to lineup slots. Bench players have no
// values, so play() only fires for players actually in the lineup.
const values = Object.fromEntries(
Object.values(lineup).filter(Boolean).map(id => [id, id]),
)
// useUmpire handles check() + play() + snapshot tracking internally.
const { check: availability, fouls } = useUmpire(lineupUmp, values, conditions)
// availability.delgado → { enabled: false, reason: 'platoon matchup' }
// fouls → [{ field: 'delgado', reason: 'platoon matchup', ... }]
// (only if Delgado was in the lineup when the pitcher flipped)

check() tells you what’s available right now. play() tells you what just fell out of play — fields that were enabled before but disabled after, and still hold values that should be cleaned up.

Playable example

Minesweeper

64 fields / 192 read-backed rules
Playing
Mines10

Not just forms.

Umpire’s field-availability model works anywhere state fits a plain object with interdependent options. This minefield models 64 cells as fields, game state as conditions, and reasons as machine-readable enums — no forms anywhere in sight.

In its simplest possible form, there are three enabledWhen rules per cell. 192 rules total. 1ms to evaluate the entire board.

Read the writeup
Define rules
import { enabledWhen, requires, umpire } from '@umpire/core'
const ump = umpire({
fields: {
email: { required: true, isEmpty: (v) => !v },
password: { required: true, isEmpty: (v) => !v },
confirmPassword: { required: true, isEmpty: (v) => !v },
referralCode: {},
companyName: {},
companySize: {},
},
rules: [
requires('confirmPassword', 'password'),
enabledWhen('companyName',
(_v, cond) => cond.plan === 'business',
{ reason: 'business plan required' }),
enabledWhen('companySize',
(_v, cond) => cond.plan === 'business',
{ reason: 'business plan required' }),
requires('companySize', 'companyName'),
],
})
Check availability
// Personal plan — company fields disabled
ump.check(
{ email: 'alex@example.com', password: 'hunter2' },
{ plan: 'personal' },
).companyName
// → { enabled: false, reason: 'business plan required' }
// Business plan — companySize requires companyName
ump.check(
{ email: 'alex@example.com', password: 'hunter2' },
{ plan: 'business' },
).companySize
// → { enabled: false, reason: 'requires companyName' }
// Switch plans — what should reset?
ump.play(
{ values: { companyName: 'Acme', companySize: '50' },
conditions: { plan: 'business' } },
{ values: { companyName: 'Acme', companySize: '50' },
conditions: { plan: 'personal' } },
)
// → [
// { field: 'companyName', suggestedValue: undefined },
// { field: 'companySize', suggestedValue: undefined },
// ]

This example is intentionally about availability, not validation. confirmPassword depends on password being present, but Umpire does not check whether the two values match. That belongs in your validation layer.

Umpire answers one question: given the current field values and conditions, which fields should be enabled, required, or due for cleanup?

Umpire is intentionally small — it handles availability so you can handle everything else with whatever tools you already use.

  • State — yours. Umpire reads values, never writes them. Use React state, Zustand, signals, a plain object — whatever owns your form.
  • Validation — yours, but composable. check() bridges validators (Zod, regex, functions) into availability rules. See Composing with Validation.
  • Rendering — yours. Umpire returns enabled: boolean per field. Hide it, disable it, dim it — your call.
  • Cleanup — yours, but guided. play() recommends which stale values to reset. You decide when and whether to apply them.
Terminal window
npm install @umpire/core

Use @umpire/core when you just need pure availability logic. Add @umpire/react, @umpire/signals, @umpire/store, or a store-specific entry point like @umpire/zustand, @umpire/redux, @umpire/pinia, @umpire/tanstack-store, or @umpire/vuex when you want an adapter on top.