UNPKG

ygocore-interface

Version:

[WIP] ygocore interface (message definitions, constants, api signatures)

524 lines (444 loc) 14.2 kB
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); }