ygocore-interface
Version:
[WIP] ygocore interface (message definitions, constants, api signatures)
524 lines (444 loc) • 14.2 kB
text/typescript
import { isQuestionMessage, LOCATION, Message, MSG, MsgUpdateCard, MsgUpdateData, MsgWin, parseMessage, POS, QUERY, Question } from './coremsg';
import { OCGEngine } from './engine';
/**
* there are packets to diliver.
*/
type STEP_DISPATCH_PACKET = 'DISPATCH_PACKET';
/**
* ocgengine is asking a duelist a question.
*/
type STEP_ASK_QUESTION = 'ASK_QUESTION';
/**
* duel finished.
*/
type STEP_DUEL_FINISHED = 'DUEL_FINISHED';
/**
* player's operation timed out.
*/
type FINISHED_REASON_TIMEOUT = 'REASON_TIMEOUT';
/**
* we've got a winner.
*/
type FINISHED_REASON_WIN = 'REASON_WIN';
/**
* something went wrong.
*/
type FINISHED_REASON_ERROR = 'REASON_ERROR';
// reasons
interface ReasonTimeout {
tag: FINISHED_REASON_TIMEOUT;
player: number;
}
interface ReasonError {
tag: FINISHED_REASON_ERROR;
error: Error;
}
interface ReasonWin {
tag: FINISHED_REASON_WIN;
message: MsgWin;
}
export interface Packet<M> {
/**
* player id
*/
whom: number;
/**
* the message
*/
what: M;
}
/**
* duel finished
*/
export interface DuelFinished {
tag: STEP_DUEL_FINISHED;
why: ReasonError | ReasonTimeout | ReasonWin;
}
/**
* the engine says:
*/
export interface DispatchPacket {
tag: STEP_DISPATCH_PACKET;
/**
* packets to be sent to players
*/
packets: Array<Packet<Message>>;
/**
* the original message
*/
original: Message;
}
/**
* the engine wants to know:
*/
export interface AskQuestion {
tag: STEP_ASK_QUESTION;
/**
* the engine is waiting for {@param question.player}'s answer (response)
*/
question: Question;
}
export type StepResult = DuelFinished | DispatchPacket | AskQuestion;
export const DUEL_RULE_1 = 1 << 16;
export const DUEL_RULE_2 = 2 << 16;
export const DUEL_RULE_3 = 3 << 16;
export const DUEL_RULE_4 = 4 << 16;
/**
* @see DUEL
*/
export const DEFAULT_DUEL_OPTIONS = DUEL_RULE_4;
const DEFAULT_LP = 8000;
const DEFAULT_START_HAND = 5;
const DEFAULT_DRAW_COUNT = 1;
/**
* params for creating duel
*/
export interface CreateDuelParams {
/**
* players, should be of size 2
*/
players: Array<{
/**
* main deck
*/
main: number[];
/**
* extra deck
*/
extra: number[];
/**
* initial LP, defaults to 8000
*/
lp?: number;
/**
* how many cards to draw before duel starts, defaults to 5
*/
start?: number;
/**
* how many cards to draw in each turn on DP, defaults to 1
*/
draw?: number
}>;
/**
* random seed
*/
seed: number;
/**
* duel options, use DEFAULT_DUEL_OPTIONS if you have no idea.
*/
options: number;
/**
* replay mode?
*/
replay?: boolean;
}
/**
* single duel only
*
* (TAG duel is a TODO)
*/
export class Duel {
private state: DuelState;
constructor(engine: OCGEngine<{}>, params: CreateDuelParams) {
this.state = createDuel(engine, params);
}
/**
* feed player's response to this duel
* @param response player's response
* @returns false for invalid response, true otherwise
*/
feed(response: Buffer) { return feed(this.state, response); }
/**
* like engine.process, see:
*
* @see StepResult
*
* @see DispatchPacket
*
* @see AskQuestion
*
* @see DuelFinished
*/
step() { return step(this.state); }
/**
* finish the duel
*/
release() { return this.state.engine.endDuel(this.state.duel); }
}
/**
* helps pumping message
*/
class MessageQueue {
private queue: Message[] = [];
private index = 0;
constructor(private pump: () => Message[]) { }
get(): Message {
this.fill();
return this.queue[this.index++];
}
peek(): Message {
this.fill();
return this.queue[this.index];
}
private fill() {
while (this.index === this.queue.length) {
this.queue = this.pump();
this.index = 0;
}
}
}
interface DuelState {
options: {
replay: boolean;
}
engine: OCGEngine<any>;
duel: any;
queue: MessageQueue;
pendingQuestion?: Question;
finished?: DuelFinished;
}
/**
* create & prepare for a duel
* @param engine the engine
* @param params configurations about this duel
*/
function createDuel(engine: OCGEngine<{}>, params: CreateDuelParams): DuelState {
const duel = engine.createDuel(params.seed);
params.players.forEach((player, playerId) => {
const lp = player.lp || DEFAULT_LP;
const draw = player.draw || DEFAULT_DRAW_COUNT;
const start = player.start || DEFAULT_START_HAND;
engine.setPlayerInfo(duel, { lp, draw, start, player: playerId });
prepareCards(playerId, player.main, LOCATION.DECK);
prepareCards(playerId, player.extra, LOCATION.EXTRA);
});
engine.startDuel(duel, params.options);
const pump = () => parseMessage(engine.process(duel).data);
return {
options: { replay: !!params.replay },
queue: new MessageQueue(pump),
engine,
duel
}
function prepareCards(player: number, cards: number[], location: number) {
for (const code of cards) {
engine.newCard(duel, { player, owner: player, sequence: 0, code, location, position: POS.FACEDOWN });
}
}
}
function feed(state: DuelState, response: Buffer): boolean {
state.engine.setResponse(state.duel, response);
if (state.queue.peek().msgtype !== 'MSG_RETRY') {
delete state.pendingQuestion;
return true;
}
return false;
}
function step(state: DuelState): StepResult {
if (state.finished) { return state.finished; }
if (state.pendingQuestion) { return { tag: 'ASK_QUESTION', question: state.pendingQuestion } }
const m = state.queue.get();
if (m.msgtype === 'MSG_WIN') {
state.finished = { tag: 'DUEL_FINISHED', why: { tag: 'REASON_WIN', message: m } };
}
if (isQuestionMessage(m)) { state.pendingQuestion = m; }
const packets = state.pendingQuestion
? handleQuestion(state, m as Question)
: handleMessage(state, m);
return packets.length ? { tag: 'DISPATCH_PACKET', packets: packets, original: m } : step(state);
}
function refreshZone(state: DuelState, player: number, location: number, queryFlags: number, useCache: boolean) {
const qbuff = state.engine.queryFieldCard(state.duel, { player, location, queryFlags, useCache });
const header = [MSG.UPDATE_DATA, player, location];
return parseMessage(Buffer.concat([Buffer.from(header), qbuff]))[0] as MsgUpdateData;
}
interface RefreshPack {
location: number;
queryFlags: number;
}
const REFRESH_FLAGS_DEFAULT = {
HAND: 0x781FFF,
MZONE: 0x881FFF,
SZONE: 0x681FFF,
SINGLE: 0xF81FFF
}
const M: RefreshPack = { location: LOCATION.MZONE, queryFlags: REFRESH_FLAGS_DEFAULT.MZONE };
const S: RefreshPack = { location: LOCATION.SZONE, queryFlags: REFRESH_FLAGS_DEFAULT.SZONE };
const H: RefreshPack = { location: LOCATION.HAND, queryFlags: REFRESH_FLAGS_DEFAULT.HAND };
function refreshMany(state: DuelState, player: number, where: RefreshPack[], useCache: boolean = true) {
return where.map(({ location, queryFlags }) => refreshZone(state, player, location, queryFlags, useCache));
}
function hideCodeForUpdateData(m: MsgUpdateData, replay: boolean): MsgUpdateData {
if (replay) { return m; }
const cards = m.cards.map(card => {
if (!(card.query_flag & QUERY.CODE) || !card.info) return { ...card, code: 0 };
if (!(card.info.position & POS.FACEUP)) return { ...card, code: 0 };
return card;
});
return { ...m, cards };
}
function refreshCard(state: DuelState, player: number, location: number, sequence: number, flags: number, useCache: boolean) {
const result = state.engine.queryCard(state.duel, { player, location, queryFlags: flags, useCache, sequence });
const header = [MSG.UPDATE_CARD, player, location, sequence];
return parseMessage(Buffer.concat([Buffer.from(header), result]))[0] as MsgUpdateCard;
}
function shouldResendRefreshSingle(m: MsgUpdateCard) {
if (!('location' in m) || !('info' in m)) return false;
if (m.location === LOCATION.REMOVED && (m.info!.position & POS.FACEDOWN)) return false;
if (m.location & LOCATION.OVERLAY) return true;
const positionAwareLoc = LOCATION.MZONE + LOCATION.SZONE + LOCATION.ONFIELD + LOCATION.REMOVED;
return (m.location & positionAwareLoc) && (m.info!.position & POS.FACEUP);
}
const both = [0, 1]
function dispatch<M>(whom: number, what: M): Packet<M> { return { whom, what }; }
function another(player: number) { return 1 - player; }
function all(player: number, replay: boolean) {
return (u: MsgUpdateData) => [dispatch(player, u), dispatch(another(player), hideCodeForUpdateData(u, replay))];
}
function handleQuestion(state: DuelState, /* NOTE: will modify */ m: Question): Packet<Message>[] {
const replay = state.options.replay;
switch (m.msgtype) {
case 'MSG_SELECT_BATTLECMD':
case 'MSG_SELECT_IDLECMD':
return both
.map(player => refreshMany(state, player, [M, S, H]).map(all(player, replay)))
.reduce(flatten, [])
.reduce(flatten, []);
case 'MSG_SELECT_TRIBUTE':
case 'MSG_SELECT_CARD':
for (const card of m.selections) {
if (card.controller !== m.player) {
card.code = 0;
}
}
break;
case 'MSG_SELECT_UNSELECT_CARD':
for (const card of m.not_selected) {
if (card.controller !== m.player) {
card.code = 0;
}
}
for (const card of m.selected) {
if (card.controller !== m.player) {
card.code = 0;
}
}
break;
default:
/* nothing to do */
}
return [];
}
function handleMessage(state: DuelState, m: Message) {
const packets: Packet<Message>[] = [];
_handleMessage(state, m, packets);
return packets;
function _handleMessage(state: DuelState, m: Message, out: Packet<Message>[]) {
const replay = state.options.replay;
function tell(whom: number, what: Message) { out.push(dispatch(whom, what)); }
function yell(what: Message) { tell(0, what); tell(1, what); }
function secretlyTellMany(whom: number) {
return (what: MsgUpdateData) => {
tell(whom, what);
tell(another(whom), hideCodeForUpdateData(what, replay));
}
}
function secretlyTell(whom: number, what: MsgUpdateCard) {
tell(whom, what);
if (shouldResendRefreshSingle(what)) tell(another(whom), what);
}
switch (m.msgtype) {
case 'MSG_HINT':
switch (m.type) {
case 1: case 2: case 3: case 5: return tell(m.player, m);
case 4: case 6: case 7: case 8: case 9: return tell(another(m.player), m);
default: return yell(m);
}
case 'MSG_CONFIRM_CARDS':
if (m.cards[0].location !== LOCATION.DECK) {
return yell(m);
} else {
return tell(m.player, m);
}
case 'MSG_SHUFFLE_HAND':
case 'MSG_SHUFFLE_EXTRA':
tell(m.player, m);
return tell(another(m.player), { ...m, cards: m.cards.map(() => 0) });
case 'MSG_SHUFFLE_SET_CARD':
for (const player of both) {
tell(player, m);
refreshMany(state, player, [{ location: m.location, queryFlags: 0x181FFF }], false).forEach(secretlyTellMany(player))
}
return;
case 'MSG_NEW_PHASE':
case 'MSG_NEW_TURN':
for (const player of both) {
refreshMany(state, player, [M, S, H]).forEach(secretlyTellMany(player));
tell(player, m);
}
return;
case 'MSG_MOVE':
tell(m.current.controller, m);
const graveOrOverlay = !!(m.current.location & (LOCATION.GRAVE + LOCATION.OVERLAY));
const deckOrHand = !!(m.current.location & (LOCATION.DECK + LOCATION.HAND));
const faceDown = !!(m.current.position & POS.FACEDOWN);
if (!graveOrOverlay && (deckOrHand || faceDown)) {
tell(another(m.current.controller), { ...m, code: m.code });
} else {
tell(another(m.current.controller), m);
}
if (m.current.location
&& !(m.current.location & LOCATION.OVERLAY)
&& (m.current.location !== m.previous.location || m.current.controller !== m.previous.controller)) {
const q = refreshCard(state, m.current.controller, m.current.location, m.current.sequence, REFRESH_FLAGS_DEFAULT.SINGLE, false);
secretlyTell(m.current.controller, q);
}
return;
case 'MSG_POS_CHANGE':
yell(m);
if ((m.previous_position & POS.FACEDOWN) && (m.current_position & POS.FACEUP)) {
const q = refreshCard(state, m.current_controller, m.current_location, m.current_sequence, REFRESH_FLAGS_DEFAULT.SINGLE, false);
secretlyTell(m.current_controller, q);
}
return;
case 'MSG_SET':
return yell({ ...m, code: 0 });
case 'MSG_SWAP':
yell(m);
for (const info of [m.first, m.second]) {
const q = refreshCard(state, info.controller, info.location, info.sequence, REFRESH_FLAGS_DEFAULT.SINGLE, false);
secretlyTell(info.controller, q);
}
return;
case 'MSG_SUMMONED':
case 'MSG_SPSUMMONED':
case 'MSG_FLIPSUMMONED':
case 'MSG_CHAINED':
case 'MSG_CHAIN_SOLVED':
case 'MSG_CHAIN_END':
for (const player of both) {
tell(player, m);
const alsoRefreshHand = m.msgtype === 'MSG_CHAINED' || m.msgtype === 'MSG_CHAIN_SOLVED' || m.msgtype === 'MSG_CHAIN_END';
refreshMany(state, player, alsoRefreshHand ? [M, S, H] : [M, S]).forEach(secretlyTellMany(player));
}
return;
case 'MSG_CARD_SELECTED': return;
case 'MSG_DRAW':
tell(m.player, m);
return tell(another(m.player), { ...m, cards: m.cards.map(code => {
return (code & 0x80000000) ? code : 0;
}) });
case 'MSG_DAMAGE_STEP_START':
case 'MSG_DAMAGE_STEP_END':
for (const player of both) {
tell(player, m);
refreshMany(state, player, [M]).forEach(secretlyTellMany(player));
}
return;
case 'MSG_MISSED_EFFECT':
return tell(m.controller, m);
default: return yell(m);
}
}
}
function flatten<T>(previous: T[], current: T[]) { return previous.concat(current); }