purerosefallen/ygopro-jstest
YGOPro test utils in JS/TS
ygopro-jstest
YGOPro test utilities in JS/TS. This package wraps koishipro-core.js to drive OCGCore duels in Node.js, with helpers for loading YGOPro resources, replay (.yrp) tests, and scripted puzzles.
Highlights
- Create and control duels from JS/TS.
- Load real YGOPro resources (scripts + cards).
- Inspect field state, drive selections, and evaluate Lua snippets.
Install
npm i ygopro-jstestResource Loading Priority
When you provide multiple sources, the loading priority is:
cdbandscriptPath(highest priority)ygoproPath(fallback)
This means cdb and scriptPath will be used first and can override data from ygoproPath.
Quick Start: Load From YRP (Jest)
Use a replay file for deterministic tests. The recommended entry point is useYGOProTest.
import {
useYGOProTest,
OcgcoreCommonConstants,
OcgcoreScriptConstants,
YGOProMsgResponseBase,
} from 'ygopro-jstest';
describe('yrp', () => {
it('loads and inspects state', async () => {
await useYGOProTest(
{
ygoproPath: '/path/to/ygopro',
yrp: './tests/test.yrp',
},
(ctx) => {
// after loading, you should already have messages processed
expect(ctx.lastSelectMessage).toBeInstanceOf(YGOProMsgResponseBase);
const newTurnCount = ctx.allMessages.filter(
(m) => m.identifier === OcgcoreCommonConstants.MSG_NEW_TURN,
).length;
expect(newTurnCount).toBeGreaterThan(0);
const mzone = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_MZONE,
0,
);
expect(mzone.length).toBeGreaterThan(0);
},
);
});
});Load From single (Puzzle Script, Jest)
Use single to load a puzzle-like Lua script (string or .lua file).
Important: after loading with single, call SlientAdvancor() once to advance into Main Phase 1 so the duel can start responding to selections.
import {
useYGOProTest,
SlientAdvancor,
YGOProMsgSelectIdleCmd,
} from 'ygopro-jstest';
describe('single', () => {
it('runs puzzle', async () => {
await useYGOProTest(
{
ygoproPath: '/path/to/ygopro',
single: `
Debug.ReloadFieldBegin(DUEL_ATTACK_FIRST_TURN)
Debug.SetPlayerInfo(0,8000,0,0)
Debug.SetPlayerInfo(1,8000,0,0)
Debug.AddCard(28985331,0,0,LOCATION_HAND,0,POS_FACEUP)
Debug.ReloadFieldEnd()
`,
},
(ctx) =>
ctx
.advance(SlientAdvancor())
.state(YGOProMsgSelectIdleCmd, (msg) => {
// respond to idle selection
}),
);
});
});Advanced: Create Duel Directly (Jest)
When you do not use yrp or single, you are responsible for building the field yourself. This is the most flexible mode and is the basis for the standalone specs.
import {
useYGOProTest,
OcgcoreScriptConstants,
SlientAdvancor,
SummonPlaceAdvancor,
NoEffectAdvancor,
YGOProMsgSelectIdleCmd,
} from 'ygopro-jstest';
describe('standalone', () => {
it('builds field and plays', async () => {
await useYGOProTest(
{
ygoproPath: '/path/to/ygopro',
},
(ctx) =>
ctx
.addCard([
{ code: 28985331, location: OcgcoreScriptConstants.LOCATION_HAND },
{ code: 10000000, location: OcgcoreScriptConstants.LOCATION_HAND },
])
.advance(SlientAdvancor())
.state(YGOProMsgSelectIdleCmd, (msg) => {
const hand = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_HAND,
);
return hand[0].summon();
})
.advance(SummonPlaceAdvancor(), NoEffectAdvancor()),
);
});
});Core Concepts
advance(...)
Advances the duel processing loop. It repeatedly calls duel.process(), and when a response is required, it invokes your advancor(s). It continues until the duel ends or no response is produced.
Example (Jest)
import { useYGOProTest, SlientAdvancor } from 'ygopro-jstest';
describe('advance', () => {
it('steps to next selection', async () => {
await useYGOProTest(
{ ygoproPath: '/path/to/ygopro', yrp: './tests/test.yrp' },
(ctx) => {
ctx.advance(SlientAdvancor());
expect(ctx.lastSelectMessage).toBeDefined();
},
);
});
});state(...)
A convenience wrapper around the last selectable message. It gives you lastSelectMessage, lets you generate a response or advancor, then internally calls advance(...) to continue.
Important: when you use the typed overload state(SomeMessageClass, cb), it will throw if the current message is not an instance of that class.
Example (Jest)
import {
useYGOProTest,
YGOProMsgSelectIdleCmd,
SlientAdvancor,
} from 'ygopro-jstest';
describe('state', () => {
it('handles a specific message type', async () => {
await useYGOProTest(
{ ygoproPath: '/path/to/ygopro', yrp: './tests/test.yrp' },
(ctx) =>
ctx
.advance(SlientAdvancor())
.state(YGOProMsgSelectIdleCmd, (msg) => {
return msg.prepareResponse(0);
}),
);
});
});evaluate(script: string)
Injects a Lua snippet into the current duel and serializes the return value back to JS.
Return value handling:
Cardis serialized asCardHandle.Groupis serialized asCardHandle[].
Example (Jest)
import { useYGOProTest } from 'ygopro-jstest';
describe('evaluate', () => {
it('returns card and group', async () => {
await useYGOProTest(
{ ygoproPath: '/path/to/ygopro', yrp: './tests/test.yrp' },
(ctx) => {
const result = ctx.evaluate(`
local g = Duel.GetFieldGroup(0, LOCATION_MZONE, 0)
local c = g:GetFirst()
return { card = c, group = g }
`);
expect(result.card).toBeDefined();
expect(Array.isArray(result.group)).toBe(true);
},
);
});
});API
useYGOProTest(options, cb)
Usage
await useYGOProTest(
{ ygoproPath: '/path/to/ygopro', yrp: './tests/test.yrp' },
(ctx) => {
// use ctx
},
);Creates a YGOProTest instance and automatically calls end() after the callback finishes.
createYGOProTest(options)
Usage
const test = await createYGOProTest({ ygoproPath: '/path/to/ygopro' });Creates a YGOProTest instance directly. You must call end() manually.
YGOProTestOptions
Loading-related fields (priority: cdb / scriptPath > ygoproPath):
ygoproPath?: string | string[]cdb?: string | Uint8Array | Database | Array<...>scriptPath?: string | string[]
Other fields:
sqljsOptions?: SqlJsConfigocgcoreOptions?: CreateOcgcoreWrapperOptionsyrp?: string | Uint8Array | YGOProYrpsingle?: stringopt?: numberplayerInfo?: { startLp?: number; startHand?: number; drawCount?: number }[]seed?: number | number[]
class YGOProTest
Properties
currentMessages: YGOProMsgBase[]allMessages: YGOProMsgBase[]currentResponses: Uint8Array[]allResponses: Uint8Array[]lastSelectMessage: YGOProMsgResponseBase | nullended: boolean
Methods
-
advance(...advancorsOrResponses)Usage
ctx.advance(SlientAdvancor());
-
state(cb | (msgClass, cb))Usage
ctx.state(YGOProMsgSelectIdleCmd, (msg) => { return msg.prepareResponse(0); });
-
evaluate(script: string): anyUsage
const result = ctx.evaluate('return Duel.GetTurnPlayer()');
-
addCard(cards)Usage
ctx.addCard({ code: 28985331, location: OcgcoreScriptConstants.LOCATION_HAND });
-
getCard(cardLocation, forced = false)Usage
const card = ctx.getCard({ controller: 0, location: LOCATION_HAND, sequence: 0 });
-
getFieldCard(player, selfLocations, oppLocations = 0)Usage
const hand = ctx.getFieldCard(0, OcgcoreScriptConstants.LOCATION_HAND);
-
getLP(player)Usage
const lp = ctx.getLP(0);
-
end()Usage
ctx.end();
class CardHandle
A CardHandle is a live handle to a specific card location in the duel. Many actions return response bytes that you must return from state(...) so advance(...) can continue.
Typical pattern
ctx.state(YGOProMsgSelectIdleCmd, (msg) => {
const hand = ctx.getFieldCard(0, OcgcoreScriptConstants.LOCATION_HAND);
return hand[0].summon();
});Core fields
controller: numberlocation: numbersequence: number
Utility
getLocationInfo()
Idle command actions
canSummon()/summon()canSpecialSummon()/specialSummon()canMset()/mset()canSset()/sset()canChangePosition()/changePosition()canActivate(desc?: number)/activate(desc?: number)
Battle command actions
canPerformAttack()/performAttack()canDirectAttack()canActivate(desc?: number)/activate(desc?: number)
Selection actions
canSelect()/select()
Important
All action methods (summon, activate, select, etc.) return response bytes. Always return them from state(...).
Advancors
Advancors are small response producers. You can pass multiple advancors into advance(...) and they will be combined. The first one that returns a response for the current message wins.
SlientAdvancor()
Calls defaultResponse() for any message. In practice, this auto-answers optional effect prompts with “do not activate” and is ideal for fast-forwarding.
NoEffectAdvancor()
Only responds to SelectChain when there are no chains available, allowing the duel to continue. It does not auto-decline effect prompts. Use this when you want to handle effect prompts yourself via state(...).
SummonPlaceAdvancor(placeAndPosition?)
Auto-selects summon placement (SelectPlace) and position (SelectPosition). You can pass a partial filter to constrain player/location/sequence/position.
SelectCardAdvancor(...filters)
Selects cards by matching filters (e.g., code, location, controller). Supports several message types like SelectCard, SelectUnselectCard, SelectSum, SelectTribute.
StaticAdvancor(items)
Returns a fixed sequence of responses you provide. Each call consumes one item.
CombinedAdvancor(...advancors)
Runs advancors in order and returns the first non-undefined response. This is the same combiner used by advance(...) internally.
MapAdvancor(...handlers)
Dispatches by message class. Each handler maps a message type to an advancor function.
MapAdvancorHandler(msgClass, cb)
Helper for building MapAdvancor handler objects.
LimitAdvancor(advancor, limit)
Wraps an advancor and only allows it to return a response limit times.
OnceAdvancor(advancor)
Shorthand for LimitAdvancor(advancor, 1).
PlayerViewAdvancor(player, advancor)
Runs the inner advancor only when responsePlayer() matches the specified player.
Composition
You can combine advancors to form a pipeline:
ctx.advance(
SlientAdvancor(),
SummonPlaceAdvancor(),
SelectCardAdvancor({ code: 28985331 }),
);Notes
- This library does not run the full YGOPro client. It drives OCGCore logic only.
- When using
single, callSlientAdvancor()once beforestate(...)to enter Main Phase 1.