Commit bf54d780 authored by Chunchi Che's avatar Chunchi Che

refactor ai predict

parent e0e5c7a6
Pipeline #28403 passed with stages
in 8 minutes and 46 seconds
export * from "./create"; export * from "./create";
export * from "./delete"; export * from "./delete";
export * from "./predict"; export * from "./predict";
export * from "./transaction";
import { CardMeta, ygopro } from "@/api"; // Data schema for YgoAgent Service
import { extraCardTypes } from "@/common";
import { CardType } from "@/stores/cardStore";
import GM = ygopro.StocGameMessage;
/** /**
* none for N/A or unknown or token. * none for N/A or unknown or token.
*/ */
enum Attribute { export enum Attribute {
None = "none", None = "none",
Earth = "earth", Earth = "earth",
Water = "water", Water = "water",
...@@ -18,38 +14,13 @@ enum Attribute { ...@@ -18,38 +14,13 @@ enum Attribute {
Divine = "divine", Divine = "divine",
} }
// from common.ts ATTRIBUTE_* export enum Controller {
// TODO (ygo-agent): replace literal numbers with constants
function numberToAttribute(attributeNumber: number): Attribute {
switch (attributeNumber) {
case 0x00:
return Attribute.None;
case 0x01:
return Attribute.Earth;
case 0x02:
return Attribute.Water;
case 0x04:
return Attribute.Fire;
case 0x08:
return Attribute.Wind;
case 0x10:
return Attribute.Light;
case 0x20:
return Attribute.Dark;
case 0x40:
return Attribute.Divine;
default:
throw new Error(`Unknown attribute number: ${attributeNumber}`);
}
}
enum Controller {
Me = "me", Me = "me",
Opponent = "opponent", Opponent = "opponent",
} }
// //
enum Location { export enum Location {
Deck = "deck", Deck = "deck",
Extra = "extra", Extra = "extra",
Grave = "grave", Grave = "grave",
...@@ -59,27 +30,6 @@ enum Location { ...@@ -59,27 +30,6 @@ enum Location {
SZone = "szone", SZone = "szone",
} }
function cardZoneToLocation(zone: ygopro.CardZone): Location {
switch (zone) {
case ygopro.CardZone.DECK:
return Location.Deck;
case ygopro.CardZone.HAND:
return Location.Hand;
case ygopro.CardZone.MZONE:
return Location.MZone;
case ygopro.CardZone.SZONE:
return Location.SZone;
case ygopro.CardZone.GRAVE:
return Location.Grave;
case ygopro.CardZone.REMOVED:
return Location.Removed;
case ygopro.CardZone.EXTRA:
return Location.Extra;
default:
throw new Error(`Unknown card zone: ${zone}`);
}
}
interface Place { interface Place {
controller: Controller; controller: Controller;
location: Location; location: Location;
...@@ -93,7 +43,7 @@ interface Option { ...@@ -93,7 +43,7 @@ interface Option {
code: number; code: number;
} }
interface CardLocation { export interface CardLocation {
controller: Controller; controller: Controller;
location: Location; location: Location;
/** /**
...@@ -106,30 +56,10 @@ interface CardLocation { ...@@ -106,30 +56,10 @@ interface CardLocation {
sequence: number; sequence: number;
} }
function convertController(controller: number, player: number): Controller {
return controller === player ? Controller.Me : Controller.Opponent;
}
function convertOverlaySequence(cl: ygopro.CardLocation): number {
return cl.is_overlay ? cl.overlay_sequence : -1;
}
function convertCardLocation(
cl: ygopro.CardLocation,
player: number,
): CardLocation {
return {
controller: convertController(cl.controller, player),
location: cardZoneToLocation(cl.zone),
overlay_sequence: convertOverlaySequence(cl),
sequence: cl.sequence,
};
}
/** /**
* If the monster is xyz material (overlay_sequence != -1), the position is faceup. * If the monster is xyz material (overlay_sequence != -1), the position is faceup.
*/ */
enum Position { export enum Position {
None = "none", None = "none",
FaceupAttack = "faceup_attack", FaceupAttack = "faceup_attack",
FacedownAttack = "facedown_attack", FacedownAttack = "facedown_attack",
...@@ -141,33 +71,10 @@ enum Position { ...@@ -141,33 +71,10 @@ enum Position {
Defense = "defense", Defense = "defense",
} }
function convertPosition(position: ygopro.CardPosition): Position {
switch (position) {
case ygopro.CardPosition.FACEUP_ATTACK:
return Position.FaceupAttack;
case ygopro.CardPosition.FACEDOWN_ATTACK:
return Position.FacedownAttack;
case ygopro.CardPosition.FACEUP_DEFENSE:
return Position.FaceupDefense;
case ygopro.CardPosition.FACEDOWN_DEFENSE:
return Position.FacedownDefense;
case ygopro.CardPosition.FACEUP:
return Position.Faceup;
case ygopro.CardPosition.FACEDOWN:
return Position.Facedown;
case ygopro.CardPosition.ATTACK:
return Position.Attack;
case ygopro.CardPosition.DEFENSE:
return Position.Defense;
default:
throw new Error(`Unknown card position: ${position}`);
}
}
/** /**
* none for N/A or unknown or token. * none for N/A or unknown or token.
*/ */
enum Race { export enum Race {
Aqua = "aqua", Aqua = "aqua",
Beast = "beast", Beast = "beast",
BeastWarrior = "beast_warrior", BeastWarrior = "beast_warrior",
...@@ -197,68 +104,7 @@ enum Race { ...@@ -197,68 +104,7 @@ enum Race {
Zombie = "zombie", Zombie = "zombie",
} }
// from common.ts RACE_* export enum Type {
// TODO (ygo-agent): replace literal numbers with constants
function numberToRace(raceNumber: number): Race {
switch (raceNumber) {
case 0x0:
return Race.None;
case 0x1:
return Race.Warrior;
case 0x2:
return Race.Spellcaster;
case 0x4:
return Race.Fairy;
case 0x8:
return Race.Fiend;
case 0x10:
return Race.Zombie;
case 0x20:
return Race.Machine;
case 0x40:
return Race.Aqua;
case 0x80:
return Race.Pyro;
case 0x100:
return Race.Rock;
case 0x200:
return Race.Windbeast;
case 0x400:
return Race.Plant;
case 0x800:
return Race.Insect;
case 0x1000:
return Race.Thunder;
case 0x2000:
return Race.Dragon;
case 0x4000:
return Race.Beast;
case 0x8000:
return Race.BeastWarrior;
case 0x10000:
return Race.Dinosaur;
case 0x20000:
return Race.Fish;
case 0x40000:
return Race.SeaSerpent;
case 0x80000:
return Race.Reptile;
case 0x100000:
return Race.Psycho;
case 0x200000:
return Race.Devine;
case 0x400000:
return Race.CreatorGod;
case 0x800000:
return Race.Wyrm;
case 0x1000000:
return Race.Cyberse;
default:
throw new Error(`Unknown race number: ${raceNumber}`);
}
}
enum Type {
Continuous = "continuous", Continuous = "continuous",
Counter = "counter", Counter = "counter",
Dual = "dual", Dual = "dual",
...@@ -286,66 +132,7 @@ enum Type { ...@@ -286,66 +132,7 @@ enum Type {
Xyz = "xyz", Xyz = "xyz",
} }
// from common.ts TYPE_* export interface Card {
// TODO (ygo-agent): replace literal numbers with constants
function numberToType(typeNumber: number): Type {
switch (typeNumber) {
case 0x1:
return Type.Monster;
case 0x2:
return Type.Spell;
case 0x4:
return Type.Trap;
case 0x10:
return Type.Normal;
case 0x20:
return Type.Effect;
case 0x40:
return Type.Fusion;
case 0x80:
return Type.Ritual;
case 0x100:
return Type.TrapMonster;
case 0x200:
return Type.Spirit;
case 0x400:
return Type.Union;
case 0x800:
return Type.Dual;
case 0x1000:
return Type.Tuner;
case 0x2000:
return Type.Synchro;
case 0x4000:
return Type.Token;
case 0x10000:
return Type.QuickPlay;
case 0x20000:
return Type.Continuous;
case 0x40000:
return Type.Equip;
case 0x80000:
return Type.Field;
case 0x100000:
return Type.Counter;
case 0x200000:
return Type.Flip;
case 0x400000:
return Type.Toon;
case 0x800000:
return Type.Xyz;
case 0x1000000:
return Type.Pendulum;
case 0x2000000:
return Type.Special;
case 0x4000000:
return Type.Link;
default:
throw new Error(`Unknown type number: ${typeNumber}`);
}
}
interface Card {
/** /**
* Card code from cards.cdb * Card code from cards.cdb
*/ */
...@@ -392,56 +179,7 @@ interface Card { ...@@ -392,56 +179,7 @@ interface Card {
types: Type[]; types: Type[];
} }
function getCounter(counters: { [type: number]: number }) { export enum Phase {
if (counters) {
for (const type in counters) {
return counters[type];
}
}
return 0;
}
export function convertDeckCard(meta: CardMeta): Card {
return {
code: meta.id,
location: Location.Deck,
sequence: 0,
controller: Controller.Me,
position: Position.Facedown,
overlay_sequence: -1,
attribute: numberToAttribute(meta.data.attribute ?? 0),
race: numberToRace(meta.data.race ?? 0),
level: meta.data.level ?? 0,
counter: 0,
negated: false,
attack: meta.data.atk ?? 0,
defense: meta.data.def ?? 0,
types: extraCardTypes(meta.data.type ?? 0).map(numberToType),
};
}
export function convertCard(card: CardType, player: number): Card {
// TODO (ygo-agent): opponent's visible facedown cards (confirm_cards)
return {
code: card.code,
location: cardZoneToLocation(card.location.zone),
sequence: card.location.sequence,
controller: convertController(card.location.controller, player),
position: convertPosition(card.location.position),
overlay_sequence: convertOverlaySequence(card.location),
attribute: numberToAttribute(card.meta.data.attribute ?? 0),
race: numberToRace(card.meta.data.race ?? 0),
level: card.meta.data.level ?? 0,
counter: getCounter(card.counters),
// TODO (ygo-agent): add negated
negated: false,
attack: card.meta.data.atk ?? 0,
defense: card.meta.data.def ?? 0,
types: extraCardTypes(card.meta.data.type ?? 0).map(numberToType),
};
}
enum Phase {
Battle = "battle", Battle = "battle",
BattleStart = "battle_start", BattleStart = "battle_start",
BattleStep = "battle_step", BattleStep = "battle_step",
...@@ -454,35 +192,6 @@ enum Phase { ...@@ -454,35 +192,6 @@ enum Phase {
Standby = "standby", Standby = "standby",
} }
import _Phase = GM.MsgNewPhase.PhaseType;
export function convertPhase(phase: _Phase): Phase {
switch (phase) {
case _Phase.DRAW:
return Phase.Draw;
case _Phase.STANDBY:
return Phase.Standby;
case _Phase.MAIN1:
return Phase.Main1;
case _Phase.BATTLE_START:
return Phase.BattleStart;
case _Phase.BATTLE_STEP:
return Phase.BattleStep;
case _Phase.DAMAGE_GAL:
return Phase.DamageCalculation;
case _Phase.DAMAGE:
return Phase.Damage;
case _Phase.BATTLE:
return Phase.Battle;
case _Phase.MAIN2:
return Phase.Main2;
case _Phase.END:
return Phase.End;
default:
throw new Error(`Unknown phase: ${phase}`);
}
}
export interface Global { export interface Global {
/** /**
* Whether me is the first player * Whether me is the first player
...@@ -495,33 +204,12 @@ export interface Global { ...@@ -495,33 +204,12 @@ export interface Global {
turn: number; turn: number;
} }
export function parsePlayerFromMsg(msg: GM): number {
if (
msg instanceof GM.MsgSelectCard ||
msg instanceof GM.MsgSelectTribute ||
msg instanceof GM.MsgSelectSum ||
msg instanceof GM.MsgSelectIdleCmd ||
msg instanceof GM.MsgSelectChain ||
msg instanceof GM.MsgSelectPosition ||
msg instanceof GM.MsgSelectEffectYn ||
msg instanceof GM.MsgSelectYesNo ||
msg instanceof GM.MsgSelectBattleCmd ||
msg instanceof GM.MsgSelectUnselectCard ||
msg instanceof GM.MsgSelectOption ||
msg instanceof GM.MsgSelectPlace ||
msg instanceof GM.MsgAnnounce
) {
return msg.player;
}
throw new Error(`Unsupported message type: ${msg}`);
}
interface SelectAbleCard { interface SelectAbleCard {
location: CardLocation; location: CardLocation;
response: number; response: number;
} }
interface MsgSelectCard { export interface MsgSelectCard {
msg_type: "select_card"; msg_type: "select_card";
cancelable: boolean; cancelable: boolean;
min: number; min: number;
...@@ -532,28 +220,13 @@ interface MsgSelectCard { ...@@ -532,28 +220,13 @@ interface MsgSelectCard {
export type MultiSelectMsg = MsgSelectCard | MsgSelectSum | MsgSelectTribute; export type MultiSelectMsg = MsgSelectCard | MsgSelectSum | MsgSelectTribute;
function convertMsgSelectCard(msg: GM.MsgSelectCard): MsgSelectCard {
// response is -1 for finish
return {
msg_type: "select_card",
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
cards: msg.cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
response: c.response,
})),
selected: [],
};
}
interface SelectTributeCard { interface SelectTributeCard {
location: CardLocation; location: CardLocation;
level: number; level: number;
response: number; response: number;
} }
interface MsgSelectTribute { export interface MsgSelectTribute {
msg_type: "select_tribute"; msg_type: "select_tribute";
cancelable: boolean; cancelable: boolean;
min: number; min: number;
...@@ -562,21 +235,6 @@ interface MsgSelectTribute { ...@@ -562,21 +235,6 @@ interface MsgSelectTribute {
selected: number[]; selected: number[];
} }
function convertMsgSelectTribute(msg: GM.MsgSelectTribute): MsgSelectTribute {
return {
msg_type: "select_tribute",
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
cards: msg.selectable_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
level: c.level,
response: c.response,
})),
selected: [],
};
}
interface SelectSumCard { interface SelectSumCard {
location: CardLocation; location: CardLocation;
level1: number; level1: number;
...@@ -595,46 +253,14 @@ export interface MsgSelectSum { ...@@ -595,46 +253,14 @@ export interface MsgSelectSum {
selected: number[]; selected: number[];
} }
function convertMsgSelectSum(msg: GM.MsgSelectSum): MsgSelectSum { export interface CardInfo {
return {
msg_type: "select_sum",
overflow: msg.overflow !== 0,
level_sum: msg.level_sum,
min: msg.min,
max: msg.max,
cards: msg.selectable_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
level1: c.level1,
level2: c.level2,
response: c.response,
})),
must_cards: msg.must_select_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
level1: c.level1,
level2: c.level2,
response: c.response,
})),
selected: [],
};
}
interface CardInfo {
code: number; code: number;
controller: Controller; controller: Controller;
location: Location; location: Location;
sequence: number; sequence: number;
} }
function convertCardInfo(cardInfo: ygopro.CardInfo, player: number): CardInfo { export enum IdleCmdType {
return {
code: cardInfo.code,
controller: convertController(cardInfo.controller, player),
location: cardZoneToLocation(cardInfo.location),
sequence: cardInfo.sequence,
};
}
enum IdleCmdType {
Summon = "summon", Summon = "summon",
SpSummon = "sp_summon", SpSummon = "sp_summon",
Reposition = "reposition", Reposition = "reposition",
...@@ -645,244 +271,83 @@ enum IdleCmdType { ...@@ -645,244 +271,83 @@ enum IdleCmdType {
ToEp = "to_ep", ToEp = "to_ep",
} }
import _IdleType = GM.MsgSelectIdleCmd.IdleCmd.IdleType;
function convertIdleCmdType(cmdType: _IdleType): IdleCmdType {
switch (cmdType) {
case _IdleType.ACTIVATE:
return IdleCmdType.Activate;
case _IdleType.MSET:
return IdleCmdType.Mset;
case _IdleType.POS_CHANGE:
return IdleCmdType.Reposition;
case _IdleType.SSET:
return IdleCmdType.Set;
case _IdleType.SPSUMMON:
return IdleCmdType.SpSummon;
case _IdleType.SUMMON:
return IdleCmdType.Summon;
case _IdleType.TO_BP:
return IdleCmdType.ToBp;
case _IdleType.TO_EP:
return IdleCmdType.ToEp;
default:
throw new Error(`Unknown idle command type: ${cmdType}`);
}
}
interface IdleCmdData { interface IdleCmdData {
card_info: CardInfo; card_info: CardInfo;
effect_description: number; effect_description: number;
response: number; response: number;
} }
interface IdleCmd { export interface IdleCmd {
cmd_type: IdleCmdType; cmd_type: IdleCmdType;
data?: IdleCmdData; data?: IdleCmdData;
} }
interface MsgSelectIdleCmd { export interface MsgSelectIdleCmd {
msg_type: "select_idlecmd"; msg_type: "select_idlecmd";
idle_cmds: IdleCmd[]; idle_cmds: IdleCmd[];
} }
function convertMsgSelectIdleCmd(msg: GM.MsgSelectIdleCmd): MsgSelectIdleCmd { export interface Chain {
const idle_cmds: IdleCmd[] = [];
for (const cmd of msg.idle_cmds) {
for (const data of cmd.idle_datas) {
const cmd_type = convertIdleCmdType(cmd.idle_type);
if (
cmd_type === IdleCmdType.Summon ||
cmd_type === IdleCmdType.SpSummon ||
cmd_type === IdleCmdType.Reposition ||
cmd_type === IdleCmdType.Mset ||
cmd_type === IdleCmdType.Set ||
cmd_type === IdleCmdType.Activate
) {
idle_cmds.push({
cmd_type,
data: {
card_info: convertCardInfo(data.card_info, msg.player),
effect_description:
cmd_type === IdleCmdType.Activate ? data.effect_description : 0,
response: data.response,
},
});
} else {
throw new Error(`Unsupported idle command type: ${cmd_type}`);
}
}
}
if (msg.enable_bp) {
// response will be 6
idle_cmds.push({ cmd_type: IdleCmdType.ToBp });
}
// TODO (ygo-agent): new models will support it
if (msg.enable_ep && !msg.enable_bp) {
// response will be 7
idle_cmds.push({ cmd_type: IdleCmdType.ToEp });
}
return {
msg_type: "select_idlecmd",
idle_cmds: idle_cmds,
};
}
interface Chain {
code: number; code: number;
location: CardLocation; location: CardLocation;
effect_description: number; effect_description: number;
response: number; response: number;
} }
interface MsgSelectChain { export interface MsgSelectChain {
msg_type: "select_chain"; msg_type: "select_chain";
forced: boolean; forced: boolean;
chains: Chain[]; chains: Chain[];
} }
function convertChain(chain: GM.MsgSelectChain.Chain, player: number): Chain { export interface MsgSelectPosition {
return {
code: chain.code,
location: convertCardLocation(chain.location, player),
effect_description: chain.effect_description,
response: chain.response,
};
}
function convertMsgSelectChain(msg: GM.MsgSelectChain): MsgSelectChain {
// response is -1 for cancel
return {
msg_type: "select_chain",
forced: msg.forced,
chains: msg.chains.map((c) => convertChain(c, msg.player)),
};
}
interface MsgSelectPosition {
msg_type: "select_position"; msg_type: "select_position";
code: number; code: number;
positions: Position[]; positions: Position[];
} }
function convertMsgSelectPosition( export interface MsgSelectYesNo {
msg: GM.MsgSelectPosition,
): MsgSelectPosition {
return {
msg_type: "select_position",
code: msg.code,
// response will be equal to POS_* from ocgcore
// POS_FACEUP_ATTACK: 0x1, POS_FACEDOWN_ATTACK: 0x2,
// POS_FACEUP_DEFENSE: 0x4, POS_FACEDOWN_DEFENSE: 0x8
positions: msg.positions.map((p) => convertPosition(p.position)),
};
}
interface MsgSelectYesNo {
msg_type: "select_yesno"; msg_type: "select_yesno";
effect_description: number; effect_description: number;
} }
function convertMsgSelectYesNo(msg: GM.MsgSelectYesNo): MsgSelectYesNo { export interface MsgSelectEffectYn {
// response is 1 for yes and 0 for no
return {
msg_type: "select_yesno",
effect_description: msg.effect_description,
};
}
interface MsgSelectEffectYn {
msg_type: "select_effectyn"; msg_type: "select_effectyn";
code: number; code: number;
location: CardLocation; location: CardLocation;
effect_description: number; effect_description: number;
} }
function convertMsgSelectEffectYn( export enum BattleCmdType {
msg: GM.MsgSelectEffectYn,
): MsgSelectEffectYn {
// response is 1 for yes and 0 for no
return {
msg_type: "select_effectyn",
code: msg.code,
location: convertCardLocation(msg.location, msg.player),
effect_description: msg.effect_description,
};
}
enum BattleCmdType {
Attack = "attack", Attack = "attack",
Activate = "activate", Activate = "activate",
ToM2 = "to_m2", ToM2 = "to_m2",
ToEp = "to_ep", ToEp = "to_ep",
} }
import _BattleCmdType = GM.MsgSelectBattleCmd.BattleCmd.BattleType; export interface BattleCmdData {
function convertBattleCmdType(cmdType: _BattleCmdType): BattleCmdType {
switch (cmdType) {
case _BattleCmdType.ATTACK:
return BattleCmdType.Attack;
case _BattleCmdType.ACTIVATE:
return BattleCmdType.Activate;
default:
throw new Error(`Unknown battle command type: ${cmdType}`);
}
}
interface BattleCmdData {
card_info: CardInfo; card_info: CardInfo;
effect_description: number; effect_description: number;
direct_attackable: boolean; direct_attackable: boolean;
response: number; response: number;
} }
interface BattleCmd { export interface BattleCmd {
cmd_type: BattleCmdType; cmd_type: BattleCmdType;
data?: BattleCmdData; data?: BattleCmdData;
} }
interface MsgSelectBattleCmd { export interface MsgSelectBattleCmd {
msg_type: "select_battlecmd"; msg_type: "select_battlecmd";
battle_cmds: BattleCmd[]; battle_cmds: BattleCmd[];
} }
function convertMsgSelectBattleCmd( export interface SelectUnselectCard {
msg: GM.MsgSelectBattleCmd,
): MsgSelectBattleCmd {
const battle_cmds: BattleCmd[] = [];
for (const cmd of msg.battle_cmds) {
const cmd_type = convertBattleCmdType(cmd.battle_type);
for (const data of cmd.battle_datas) {
const battle_data: BattleCmdData = {
card_info: convertCardInfo(data.card_info, msg.player),
effect_description: data.effect_description,
direct_attackable: data.direct_attackable,
response: data.response,
};
battle_cmds.push({ cmd_type, data: battle_data });
}
}
if (msg.enable_m2) {
// response will be 2
battle_cmds.push({ cmd_type: BattleCmdType.ToM2 });
}
// TODO (ygo-agent): new models will support it
if (msg.enable_ep && !msg.enable_m2) {
// response will be 3
battle_cmds.push({ cmd_type: BattleCmdType.ToEp });
}
return {
msg_type: "select_battlecmd",
battle_cmds,
};
}
interface SelectUnselectCard {
location: CardLocation; location: CardLocation;
response: number; response: number;
} }
interface MsgSelectUnselectCard { export interface MsgSelectUnselectCard {
msg_type: "select_unselect_card"; msg_type: "select_unselect_card";
finishable: boolean; finishable: boolean;
cancelable: boolean; cancelable: boolean;
...@@ -892,118 +357,50 @@ interface MsgSelectUnselectCard { ...@@ -892,118 +357,50 @@ interface MsgSelectUnselectCard {
selectable_cards: SelectUnselectCard[]; selectable_cards: SelectUnselectCard[];
} }
function convertMsgSelectUnselectCard(
msg: GM.MsgSelectUnselectCard,
): MsgSelectUnselectCard {
return {
msg_type: "select_unselect_card",
// response is -1 for finish
finishable: msg.finishable,
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
selected_cards: msg.selected_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
response: c.response,
})),
selectable_cards: msg.selectable_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
response: c.response,
})),
};
}
interface Option { interface Option {
code: number; code: number;
response: number; response: number;
} }
interface MsgSelectOption { export interface MsgSelectOption {
msg_type: "select_option"; msg_type: "select_option";
options: Option[]; options: Option[];
} }
function convertMsgSelectOption(msg: GM.MsgSelectOption): MsgSelectOption {
return {
msg_type: "select_option",
options: msg.options.map((o) => ({
code: o.code,
response: o.response,
})),
};
}
interface Place { interface Place {
controller: Controller; controller: Controller;
location: Location; location: Location;
sequence: number; sequence: number;
} }
interface MsgSelectPlace { export interface MsgSelectPlace {
msg_type: "select_place"; msg_type: "select_place";
count: number; count: number;
places: Place[]; places: Place[];
} }
// TODO (ygo-agent): SelectDisfield is different from SelectPlace
function convertMsgSelectPlace(msg: GM.MsgSelectPlace): MsgSelectPlace {
return {
msg_type: "select_place",
count: msg.count,
places: msg.places.map((p) => ({
// NOTICE: the response is the index of the place in the places array
controller: convertController(p.controller, msg.player),
location: cardZoneToLocation(p.zone),
sequence: p.sequence,
})),
};
}
interface AnnounceAttrib { interface AnnounceAttrib {
attribute: Attribute; attribute: Attribute;
response: number; response: number;
} }
interface MsgAnnounceAttrib { export interface MsgAnnounceAttrib {
msg_type: "announce_attrib"; msg_type: "announce_attrib";
count: number; count: number;
attributes: AnnounceAttrib[]; attributes: AnnounceAttrib[];
} }
function convertMsgAnnounceAttrib(msg: GM.MsgAnnounce): MsgAnnounceAttrib {
return {
msg_type: "announce_attrib",
count: msg.min,
// from api/ocgcore/ocgAdapter/stoc/stocGameMsg/announceAttrib.ts
attributes: msg.options.map((a) => ({
attribute: numberToAttribute(1 << a.code),
response: a.response,
})),
};
}
interface AnnounceNumber { interface AnnounceNumber {
number: number; number: number;
response: number; response: number;
} }
interface MsgAnnounceNumber { export interface MsgAnnounceNumber {
msg_type: "announce_number"; msg_type: "announce_number";
count: number; count: number;
numbers: AnnounceNumber[]; numbers: AnnounceNumber[];
} }
function convertMsgAnnounceNumber(msg: GM.MsgAnnounce): MsgAnnounceNumber {
return {
msg_type: "announce_number",
count: msg.min,
numbers: msg.options.map((o) => ({
number: o.code,
response: o.response,
})),
};
}
type ActionMsgData = type ActionMsgData =
| MsgSelectCard | MsgSelectCard
| MsgSelectTribute | MsgSelectTribute
...@@ -1020,76 +417,10 @@ type ActionMsgData = ...@@ -1020,76 +417,10 @@ type ActionMsgData =
| MsgAnnounceAttrib | MsgAnnounceAttrib
| MsgAnnounceNumber; | MsgAnnounceNumber;
interface ActionMsg { export interface ActionMsg {
data: ActionMsgData; data: ActionMsgData;
} }
export function convertActionMsg(msg: ygopro.StocGameMessage): ActionMsg {
if (msg instanceof GM.MsgSelectCard) {
return {
data: convertMsgSelectCard(msg),
};
} else if (msg instanceof GM.MsgSelectTribute) {
return {
data: convertMsgSelectTribute(msg),
};
} else if (msg instanceof GM.MsgSelectSum) {
return {
data: convertMsgSelectSum(msg),
};
} else if (msg instanceof GM.MsgSelectIdleCmd) {
return {
data: convertMsgSelectIdleCmd(msg),
};
} else if (msg instanceof GM.MsgSelectChain) {
return {
data: convertMsgSelectChain(msg),
};
} else if (msg instanceof GM.MsgSelectPosition) {
return {
data: convertMsgSelectPosition(msg),
};
} else if (msg instanceof GM.MsgSelectEffectYn) {
return {
data: convertMsgSelectEffectYn(msg),
};
} else if (msg instanceof GM.MsgSelectYesNo) {
return {
data: convertMsgSelectYesNo(msg),
};
} else if (msg instanceof GM.MsgSelectBattleCmd) {
return {
data: convertMsgSelectBattleCmd(msg),
};
} else if (msg instanceof GM.MsgSelectUnselectCard) {
return {
data: convertMsgSelectUnselectCard(msg),
};
} else if (msg instanceof GM.MsgSelectOption) {
return {
data: convertMsgSelectOption(msg),
};
} else if (msg instanceof GM.MsgSelectPlace) {
return {
data: convertMsgSelectPlace(msg),
};
} else if (msg instanceof GM.MsgAnnounce) {
if (msg.announce_type === GM.MsgAnnounce.AnnounceType.Attribute) {
return {
data: convertMsgAnnounceAttrib(msg),
};
} else if (msg.announce_type === GM.MsgAnnounce.AnnounceType.Number) {
return {
data: convertMsgAnnounceNumber(msg),
};
} else {
throw new Error(`Unsupported announce type: ${msg.announce_type}`);
}
} else {
throw new Error(`Unsupported message type: ${msg}`);
}
}
export interface Input { export interface Input {
action_msg: ActionMsg; action_msg: ActionMsg;
cards: Card[]; cards: Card[];
......
import { CardMeta, ygopro } from "@/api";
import {
ATTRIBUTE_DARK,
ATTRIBUTE_DEVINE,
ATTRIBUTE_EARTH,
ATTRIBUTE_FIRE,
ATTRIBUTE_LIGHT,
ATTRIBUTE_WATER,
ATTRIBUTE_WIND,
FACEDOWN_ATTACK,
FACEDOWN_DEFENSE,
FACEUP_ATTACK,
FACEUP_DEFENSE,
RACE_AQUA,
RACE_BEAST,
RACE_BEASTWARRIOR,
RACE_CREATORGOD,
RACE_CYBERSE,
RACE_DEVINE,
RACE_DINOSAUR,
RACE_DRAGON,
RACE_FAIRY,
RACE_FIEND,
RACE_FISH,
RACE_INSECT,
RACE_MACHINE,
RACE_PLANT,
RACE_PSYCHO,
RACE_PYRO,
RACE_REPTILE,
RACE_ROCK,
RACE_SEASERPENT,
RACE_SPELLCASTER,
RACE_THUNDER,
RACE_WARRIOR,
RACE_WINDBEAST,
RACE_WYRM,
RACE_ZOMBIE,
TYPE_CONTINUOUS,
TYPE_COUNTER,
TYPE_DUAL,
TYPE_EFFECT,
TYPE_EQUIP,
TYPE_FIELD,
TYPE_FLIP,
TYPE_FUSION,
TYPE_LINK,
TYPE_MONSTER,
TYPE_NORMAL,
TYPE_PENDULUM,
TYPE_QUICKPLAY,
TYPE_RITUAL,
TYPE_SPELL,
TYPE_SPIRIT,
TYPE_SPSUMMON,
TYPE_SYNCHRO,
TYPE_TOKEN,
TYPE_TOON,
TYPE_TRAP,
TYPE_TRAPMONSTER,
TYPE_TUNER,
TYPE_UNION,
TYPE_XYZ,
} from "@/common";
import { extraCardTypes } from "@/common";
import { CardType } from "@/stores/cardStore";
import {
ActionMsg,
Attribute,
BattleCmd,
BattleCmdData,
BattleCmdType,
Card,
CardInfo,
CardLocation,
Chain,
Controller,
IdleCmd,
IdleCmdType,
Location,
MsgAnnounceAttrib,
MsgAnnounceNumber,
MsgSelectBattleCmd,
MsgSelectCard,
MsgSelectChain,
MsgSelectEffectYn,
MsgSelectIdleCmd,
MsgSelectOption,
MsgSelectPlace,
MsgSelectPosition,
MsgSelectSum,
MsgSelectTribute,
MsgSelectUnselectCard,
MsgSelectYesNo,
Phase,
Position,
Race,
Type,
} from "./schema";
import GM = ygopro.StocGameMessage;
import _Phase = GM.MsgNewPhase.PhaseType;
import _IdleType = GM.MsgSelectIdleCmd.IdleCmd.IdleType;
import _BattleCmdType = GM.MsgSelectBattleCmd.BattleCmd.BattleType;
// from common.ts ATTRIBUTE_*
function numberToAttribute(attributeNumber: number): Attribute {
switch (attributeNumber) {
case 0x00:
return Attribute.None;
case ATTRIBUTE_EARTH:
return Attribute.Earth;
case ATTRIBUTE_WATER:
return Attribute.Water;
case ATTRIBUTE_FIRE:
return Attribute.Fire;
case ATTRIBUTE_WIND:
return Attribute.Wind;
case ATTRIBUTE_LIGHT:
return Attribute.Light;
case ATTRIBUTE_DARK:
return Attribute.Dark;
case ATTRIBUTE_DEVINE:
return Attribute.Divine;
default:
throw new Error(`Unknown attribute number: ${attributeNumber}`);
}
}
function cardZoneToLocation(zone: ygopro.CardZone): Location {
switch (zone) {
case ygopro.CardZone.DECK:
return Location.Deck;
case ygopro.CardZone.HAND:
return Location.Hand;
case ygopro.CardZone.MZONE:
return Location.MZone;
case ygopro.CardZone.SZONE:
return Location.SZone;
case ygopro.CardZone.GRAVE:
return Location.Grave;
case ygopro.CardZone.REMOVED:
return Location.Removed;
case ygopro.CardZone.EXTRA:
return Location.Extra;
default:
throw new Error(`Unknown card zone: ${zone}`);
}
}
function convertController(controller: number, player: number): Controller {
return controller === player ? Controller.Me : Controller.Opponent;
}
function convertOverlaySequence(cl: ygopro.CardLocation): number {
return cl.is_overlay ? cl.overlay_sequence : -1;
}
function convertCardLocation(
cl: ygopro.CardLocation,
player: number,
): CardLocation {
return {
controller: convertController(cl.controller, player),
location: cardZoneToLocation(cl.zone),
overlay_sequence: convertOverlaySequence(cl),
sequence: cl.sequence,
};
}
function convertPosition(position: ygopro.CardPosition): Position {
switch (position) {
case ygopro.CardPosition.FACEUP_ATTACK:
return Position.FaceupAttack;
case ygopro.CardPosition.FACEDOWN_ATTACK:
return Position.FacedownAttack;
case ygopro.CardPosition.FACEUP_DEFENSE:
return Position.FaceupDefense;
case ygopro.CardPosition.FACEDOWN_DEFENSE:
return Position.FacedownDefense;
case ygopro.CardPosition.FACEUP:
return Position.Faceup;
case ygopro.CardPosition.FACEDOWN:
return Position.Facedown;
case ygopro.CardPosition.ATTACK:
return Position.Attack;
case ygopro.CardPosition.DEFENSE:
return Position.Defense;
default:
throw new Error(`Unknown card position: ${position}`);
}
}
function numberToRace(raceNumber: number): Race {
switch (raceNumber) {
case 0x0:
return Race.None;
case RACE_WARRIOR:
return Race.Warrior;
case RACE_SPELLCASTER:
return Race.Spellcaster;
case RACE_FAIRY:
return Race.Fairy;
case RACE_FIEND:
return Race.Fiend;
case RACE_ZOMBIE:
return Race.Zombie;
case RACE_MACHINE:
return Race.Machine;
case RACE_AQUA:
return Race.Aqua;
case RACE_PYRO:
return Race.Pyro;
case RACE_ROCK:
return Race.Rock;
case RACE_WINDBEAST:
return Race.Windbeast;
case RACE_PLANT:
return Race.Plant;
case RACE_INSECT:
return Race.Insect;
case RACE_THUNDER:
return Race.Thunder;
case RACE_DRAGON:
return Race.Dragon;
case RACE_BEAST:
return Race.Beast;
case RACE_BEASTWARRIOR:
return Race.BeastWarrior;
case RACE_DINOSAUR:
return Race.Dinosaur;
case RACE_FISH:
return Race.Fish;
case RACE_SEASERPENT:
return Race.SeaSerpent;
case RACE_REPTILE:
return Race.Reptile;
case RACE_PSYCHO:
return Race.Psycho;
case RACE_DEVINE:
return Race.Devine;
case RACE_CREATORGOD:
return Race.CreatorGod;
case RACE_WYRM:
return Race.Wyrm;
case RACE_CYBERSE:
return Race.Cyberse;
default:
throw new Error(`Unknown race number: ${raceNumber}`);
}
}
function numberToType(typeNumber: number): Type {
switch (typeNumber) {
case TYPE_MONSTER:
return Type.Monster;
case TYPE_SPELL:
return Type.Spell;
case TYPE_TRAP:
return Type.Trap;
case TYPE_NORMAL:
return Type.Normal;
case TYPE_EFFECT:
return Type.Effect;
case TYPE_FUSION:
return Type.Fusion;
case TYPE_RITUAL:
return Type.Ritual;
case TYPE_TRAPMONSTER:
return Type.TrapMonster;
case TYPE_SPIRIT:
return Type.Spirit;
case TYPE_UNION:
return Type.Union;
case TYPE_DUAL:
return Type.Dual;
case TYPE_TUNER:
return Type.Tuner;
case TYPE_SYNCHRO:
return Type.Synchro;
case TYPE_TOKEN:
return Type.Token;
case TYPE_QUICKPLAY:
return Type.QuickPlay;
case TYPE_CONTINUOUS:
return Type.Continuous;
case TYPE_EQUIP:
return Type.Equip;
case TYPE_FIELD:
return Type.Field;
case TYPE_COUNTER:
return Type.Counter;
case TYPE_FLIP:
return Type.Flip;
case TYPE_TOON:
return Type.Toon;
case TYPE_XYZ:
return Type.Xyz;
case TYPE_PENDULUM:
return Type.Pendulum;
case TYPE_SPSUMMON:
return Type.Special;
case TYPE_LINK:
return Type.Link;
default:
throw new Error(`Unknown type number: ${typeNumber}`);
}
}
function getCounter(counters: { [type: number]: number }) {
if (counters) {
for (const type in counters) {
return counters[type];
}
}
return 0;
}
export function convertDeckCard(meta: CardMeta): Card {
return {
code: meta.id,
location: Location.Deck,
sequence: 0,
controller: Controller.Me,
position: Position.Facedown,
overlay_sequence: -1,
attribute: numberToAttribute(meta.data.attribute ?? 0),
race: numberToRace(meta.data.race ?? 0),
level: meta.data.level ?? 0,
counter: 0,
negated: false,
attack: meta.data.atk ?? 0,
defense: meta.data.def ?? 0,
types: extraCardTypes(meta.data.type ?? 0).map(numberToType),
};
}
export function convertCard(card: CardType, player: number): Card {
// TODO (ygo-agent): opponent's visible facedown cards (confirm_cards)
const { code, location, meta, counters } = card;
return {
code,
location: cardZoneToLocation(location.zone),
sequence: location.sequence,
controller: convertController(location.controller, player),
position: convertPosition(location.position),
overlay_sequence: convertOverlaySequence(location),
attribute: numberToAttribute(meta.data.attribute ?? 0),
race: numberToRace(meta.data.race ?? 0),
level: meta.data.level ?? 0,
counter: getCounter(counters),
// TODO (ygo-agent): add negated
negated: false,
attack: meta.data.atk ?? 0,
defense: meta.data.def ?? 0,
types: extraCardTypes(meta.data.type ?? 0).map(numberToType),
};
}
export function convertPhase(phase: _Phase): Phase {
switch (phase) {
case _Phase.DRAW:
return Phase.Draw;
case _Phase.STANDBY:
return Phase.Standby;
case _Phase.MAIN1:
return Phase.Main1;
case _Phase.BATTLE_START:
return Phase.BattleStart;
case _Phase.BATTLE_STEP:
return Phase.BattleStep;
case _Phase.DAMAGE_GAL:
return Phase.DamageCalculation;
case _Phase.DAMAGE:
return Phase.Damage;
case _Phase.BATTLE:
return Phase.Battle;
case _Phase.MAIN2:
return Phase.Main2;
case _Phase.END:
return Phase.End;
default:
throw new Error(`Unknown phase: ${phase}`);
}
}
export function parsePlayerFromMsg(msg: GM): number {
if (
msg instanceof GM.MsgSelectCard ||
msg instanceof GM.MsgSelectTribute ||
msg instanceof GM.MsgSelectSum ||
msg instanceof GM.MsgSelectIdleCmd ||
msg instanceof GM.MsgSelectChain ||
msg instanceof GM.MsgSelectPosition ||
msg instanceof GM.MsgSelectEffectYn ||
msg instanceof GM.MsgSelectYesNo ||
msg instanceof GM.MsgSelectBattleCmd ||
msg instanceof GM.MsgSelectUnselectCard ||
msg instanceof GM.MsgSelectOption ||
msg instanceof GM.MsgSelectPlace ||
msg instanceof GM.MsgAnnounce
) {
return msg.player;
}
throw new Error(`Unsupported message type: ${msg}`);
}
function convertMsgSelectCard(msg: GM.MsgSelectCard): MsgSelectCard {
// response is -1 for finish
return {
msg_type: "select_card",
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
cards: msg.cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
response: c.response,
})),
selected: [],
};
}
function convertMsgSelectTribute(msg: GM.MsgSelectTribute): MsgSelectTribute {
return {
msg_type: "select_tribute",
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
cards: msg.selectable_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
level: c.level,
response: c.response,
})),
selected: [],
};
}
function convertMsgSelectSum(msg: GM.MsgSelectSum): MsgSelectSum {
return {
msg_type: "select_sum",
overflow: msg.overflow !== 0,
level_sum: msg.level_sum,
min: msg.min,
max: msg.max,
cards: msg.selectable_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
level1: c.level1,
level2: c.level2,
response: c.response,
})),
must_cards: msg.must_select_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
level1: c.level1,
level2: c.level2,
response: c.response,
})),
selected: [],
};
}
function convertCardInfo(cardInfo: ygopro.CardInfo, player: number): CardInfo {
return {
code: cardInfo.code,
controller: convertController(cardInfo.controller, player),
location: cardZoneToLocation(cardInfo.location),
sequence: cardInfo.sequence,
};
}
function convertIdleCmdType(cmdType: _IdleType): IdleCmdType {
switch (cmdType) {
case _IdleType.ACTIVATE:
return IdleCmdType.Activate;
case _IdleType.MSET:
return IdleCmdType.Mset;
case _IdleType.POS_CHANGE:
return IdleCmdType.Reposition;
case _IdleType.SSET:
return IdleCmdType.Set;
case _IdleType.SPSUMMON:
return IdleCmdType.SpSummon;
case _IdleType.SUMMON:
return IdleCmdType.Summon;
case _IdleType.TO_BP:
return IdleCmdType.ToBp;
case _IdleType.TO_EP:
return IdleCmdType.ToEp;
default:
throw new Error(`Unknown idle command type: ${cmdType}`);
}
}
function convertMsgSelectIdleCmd(msg: GM.MsgSelectIdleCmd): MsgSelectIdleCmd {
const idle_cmds: IdleCmd[] = [];
for (const cmd of msg.idle_cmds) {
for (const data of cmd.idle_datas) {
const cmd_type = convertIdleCmdType(cmd.idle_type);
if (
cmd_type === IdleCmdType.Summon ||
cmd_type === IdleCmdType.SpSummon ||
cmd_type === IdleCmdType.Reposition ||
cmd_type === IdleCmdType.Mset ||
cmd_type === IdleCmdType.Set ||
cmd_type === IdleCmdType.Activate
) {
idle_cmds.push({
cmd_type,
data: {
card_info: convertCardInfo(data.card_info, msg.player),
effect_description:
cmd_type === IdleCmdType.Activate ? data.effect_description : 0,
response: data.response,
},
});
} else {
throw new Error(`Unsupported idle command type: ${cmd_type}`);
}
}
}
if (msg.enable_bp) {
// response will be 6
idle_cmds.push({ cmd_type: IdleCmdType.ToBp });
}
// TODO (ygo-agent): new models will support it
if (msg.enable_ep && !msg.enable_bp) {
// response will be 7
idle_cmds.push({ cmd_type: IdleCmdType.ToEp });
}
return {
msg_type: "select_idlecmd",
idle_cmds: idle_cmds,
};
}
function convertChain(chain: GM.MsgSelectChain.Chain, player: number): Chain {
return {
code: chain.code,
location: convertCardLocation(chain.location, player),
effect_description: chain.effect_description,
response: chain.response,
};
}
function convertMsgSelectChain(msg: GM.MsgSelectChain): MsgSelectChain {
// response is -1 for cancel
return {
msg_type: "select_chain",
forced: msg.forced,
chains: msg.chains.map((c) => convertChain(c, msg.player)),
};
}
function convertMsgSelectPosition(
msg: GM.MsgSelectPosition,
): MsgSelectPosition {
return {
msg_type: "select_position",
code: msg.code,
// response will be equal to POS_* from ocgcore
// POS_FACEUP_ATTACK: 0x1, POS_FACEDOWN_ATTACK: 0x2,
// POS_FACEUP_DEFENSE: 0x4, POS_FACEDOWN_DEFENSE: 0x8
positions: msg.positions.map((p) => convertPosition(p.position)),
};
}
function convertMsgSelectYesNo(msg: GM.MsgSelectYesNo): MsgSelectYesNo {
// response is 1 for yes and 0 for no
return {
msg_type: "select_yesno",
effect_description: msg.effect_description,
};
}
function convertMsgSelectEffectYn(
msg: GM.MsgSelectEffectYn,
): MsgSelectEffectYn {
// response is 1 for yes and 0 for no
return {
msg_type: "select_effectyn",
code: msg.code,
location: convertCardLocation(msg.location, msg.player),
effect_description: msg.effect_description,
};
}
function convertBattleCmdType(cmdType: _BattleCmdType): BattleCmdType {
switch (cmdType) {
case _BattleCmdType.ATTACK:
return BattleCmdType.Attack;
case _BattleCmdType.ACTIVATE:
return BattleCmdType.Activate;
default:
throw new Error(`Unknown battle command type: ${cmdType}`);
}
}
function convertMsgSelectBattleCmd(
msg: GM.MsgSelectBattleCmd,
): MsgSelectBattleCmd {
const battle_cmds: BattleCmd[] = [];
for (const cmd of msg.battle_cmds) {
const cmd_type = convertBattleCmdType(cmd.battle_type);
for (const data of cmd.battle_datas) {
const battle_data: BattleCmdData = {
card_info: convertCardInfo(data.card_info, msg.player),
effect_description: data.effect_description,
direct_attackable: data.direct_attackable,
response: data.response,
};
battle_cmds.push({ cmd_type, data: battle_data });
}
}
if (msg.enable_m2) {
// response will be 2
battle_cmds.push({ cmd_type: BattleCmdType.ToM2 });
}
// TODO (ygo-agent): new models will support it
if (msg.enable_ep && !msg.enable_m2) {
// response will be 3
battle_cmds.push({ cmd_type: BattleCmdType.ToEp });
}
return {
msg_type: "select_battlecmd",
battle_cmds,
};
}
function convertMsgSelectUnselectCard(
msg: GM.MsgSelectUnselectCard,
): MsgSelectUnselectCard {
return {
msg_type: "select_unselect_card",
// response is -1 for finish
finishable: msg.finishable,
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
selected_cards: msg.selected_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
response: c.response,
})),
selectable_cards: msg.selectable_cards.map((c) => ({
location: convertCardLocation(c.location, msg.player),
response: c.response,
})),
};
}
function convertMsgSelectOption(msg: GM.MsgSelectOption): MsgSelectOption {
return {
msg_type: "select_option",
options: msg.options.map((o) => ({
code: o.code,
response: o.response,
})),
};
}
// TODO (ygo-agent): SelectDisfield is different from SelectPlace
function convertMsgSelectPlace(msg: GM.MsgSelectPlace): MsgSelectPlace {
return {
msg_type: "select_place",
count: msg.count,
places: msg.places.map((p) => ({
// NOTICE: the response is the index of the place in the places array
controller: convertController(p.controller, msg.player),
location: cardZoneToLocation(p.zone),
sequence: p.sequence,
})),
};
}
function convertMsgAnnounceAttrib(msg: GM.MsgAnnounce): MsgAnnounceAttrib {
return {
msg_type: "announce_attrib",
count: msg.min,
// from api/ocgcore/ocgAdapter/stoc/stocGameMsg/announceAttrib.ts
attributes: msg.options.map((a) => ({
attribute: numberToAttribute(1 << a.code),
response: a.response,
})),
};
}
function convertMsgAnnounceNumber(msg: GM.MsgAnnounce): MsgAnnounceNumber {
return {
msg_type: "announce_number",
count: msg.min,
numbers: msg.options.map((o) => ({
number: o.code,
response: o.response,
})),
};
}
export function convertActionMsg(msg: ygopro.StocGameMessage): ActionMsg {
if (msg instanceof GM.MsgSelectCard) {
return {
data: convertMsgSelectCard(msg),
};
} else if (msg instanceof GM.MsgSelectTribute) {
return {
data: convertMsgSelectTribute(msg),
};
} else if (msg instanceof GM.MsgSelectSum) {
return {
data: convertMsgSelectSum(msg),
};
} else if (msg instanceof GM.MsgSelectIdleCmd) {
return {
data: convertMsgSelectIdleCmd(msg),
};
} else if (msg instanceof GM.MsgSelectChain) {
return {
data: convertMsgSelectChain(msg),
};
} else if (msg instanceof GM.MsgSelectPosition) {
return {
data: convertMsgSelectPosition(msg),
};
} else if (msg instanceof GM.MsgSelectEffectYn) {
return {
data: convertMsgSelectEffectYn(msg),
};
} else if (msg instanceof GM.MsgSelectYesNo) {
return {
data: convertMsgSelectYesNo(msg),
};
} else if (msg instanceof GM.MsgSelectBattleCmd) {
return {
data: convertMsgSelectBattleCmd(msg),
};
} else if (msg instanceof GM.MsgSelectUnselectCard) {
return {
data: convertMsgSelectUnselectCard(msg),
};
} else if (msg instanceof GM.MsgSelectOption) {
return {
data: convertMsgSelectOption(msg),
};
} else if (msg instanceof GM.MsgSelectPlace) {
return {
data: convertMsgSelectPlace(msg),
};
} else if (msg instanceof GM.MsgAnnounce) {
if (msg.announce_type === GM.MsgAnnounce.AnnounceType.Attribute) {
return {
data: convertMsgAnnounceAttrib(msg),
};
} else if (msg.announce_type === GM.MsgAnnounce.AnnounceType.Number) {
return {
data: convertMsgAnnounceNumber(msg),
};
} else {
throw new Error(`Unsupported announce type: ${msg.announce_type}`);
}
} else {
throw new Error(`Unsupported message type: ${msg}`);
}
}
export function convertPositionResponse(response: number): ygopro.CardPosition {
switch (response) {
case FACEUP_ATTACK:
return ygopro.CardPosition.FACEUP_ATTACK;
case FACEDOWN_ATTACK:
return ygopro.CardPosition.FACEDOWN_ATTACK;
case FACEUP_DEFENSE:
return ygopro.CardPosition.FACEUP_DEFENSE;
case FACEDOWN_DEFENSE:
return ygopro.CardPosition.FACEDOWN_DEFENSE;
default:
throw new Error(`Invalid position response: ${response}`);
}
}
...@@ -3,31 +3,38 @@ import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType; ...@@ -3,31 +3,38 @@ import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { CardMeta } from "@/api"; import { CardMeta } from "@/api";
//! 一些Neos中基础的数据结构 //! 一些Neos中基础的数据结构
// Position
export const FACEUP_ATTACK = 0x1;
export const FACEDOWN_ATTACK = 0x2;
export const FACEUP_DEFENSE = 0x4;
export const FACEDOWN_DEFENSE = 0x8;
// 类型 // 类型
const TYPE_MONSTER = 0x1; // export const TYPE_MONSTER = 0x1; //
const TYPE_SPELL = 0x2; // export const TYPE_SPELL = 0x2; //
const TYPE_TRAP = 0x4; // export const TYPE_TRAP = 0x4; //
const TYPE_NORMAL = 0x10; // export const TYPE_NORMAL = 0x10; //
const TYPE_EFFECT = 0x20; // export const TYPE_EFFECT = 0x20; //
const TYPE_FUSION = 0x40; // export const TYPE_FUSION = 0x40; //
const TYPE_RITUAL = 0x80; // export const TYPE_RITUAL = 0x80; //
const TYPE_TRAPMONSTER = 0x100; // export const TYPE_TRAPMONSTER = 0x100; //
const TYPE_SPIRIT = 0x200; // export const TYPE_SPIRIT = 0x200; //
const TYPE_UNION = 0x400; // export const TYPE_UNION = 0x400; //
const TYPE_DUAL = 0x800; // export const TYPE_DUAL = 0x800; //
const TYPE_TUNER = 0x1000; // export const TYPE_TUNER = 0x1000; //
const TYPE_SYNCHRO = 0x2000; // export const TYPE_SYNCHRO = 0x2000; //
export const TYPE_TOKEN = 0x4000; // export const TYPE_TOKEN = 0x4000; //
const TYPE_QUICKPLAY = 0x10000; // export const TYPE_QUICKPLAY = 0x10000; //
const TYPE_CONTINUOUS = 0x20000; // export const TYPE_CONTINUOUS = 0x20000; //
const TYPE_EQUIP = 0x40000; // export const TYPE_EQUIP = 0x40000; //
const TYPE_FIELD = 0x80000; // export const TYPE_FIELD = 0x80000; //
const TYPE_COUNTER = 0x100000; // export const TYPE_COUNTER = 0x100000; //
const TYPE_FLIP = 0x200000; // export const TYPE_FLIP = 0x200000; //
const TYPE_TOON = 0x400000; // export const TYPE_TOON = 0x400000; //
const TYPE_XYZ = 0x800000; // export const TYPE_XYZ = 0x800000; //
const TYPE_PENDULUM = 0x1000000; // export const TYPE_PENDULUM = 0x1000000; //
const TYPE_SPSUMMON = 0x2000000; // export const TYPE_SPSUMMON = 0x2000000; //
export const TYPE_LINK = 0x4000000; // export const TYPE_LINK = 0x4000000; //
/* /*
...@@ -147,13 +154,13 @@ export function isPendulumMonster(typeCode: number): boolean { ...@@ -147,13 +154,13 @@ export function isPendulumMonster(typeCode: number): boolean {
// 属性 // 属性
// const ATTRIBUTE_ALL = 0x7f; // // const ATTRIBUTE_ALL = 0x7f; //
const ATTRIBUTE_EARTH = 0x01; // export const ATTRIBUTE_EARTH = 0x01; //
const ATTRIBUTE_WATER = 0x02; // export const ATTRIBUTE_WATER = 0x02; //
const ATTRIBUTE_FIRE = 0x04; // export const ATTRIBUTE_FIRE = 0x04; //
const ATTRIBUTE_WIND = 0x08; // export const ATTRIBUTE_WIND = 0x08; //
const ATTRIBUTE_LIGHT = 0x10; // export const ATTRIBUTE_LIGHT = 0x10; //
const ATTRIBUTE_DARK = 0x20; // export const ATTRIBUTE_DARK = 0x20; //
const ATTRIBUTE_DEVINE = 0x40; // export const ATTRIBUTE_DEVINE = 0x40; //
export const Attribute2StringCodeMap: Map<number, number> = new Map([ export const Attribute2StringCodeMap: Map<number, number> = new Map([
[ATTRIBUTE_EARTH, 1010], [ATTRIBUTE_EARTH, 1010],
...@@ -166,31 +173,31 @@ export const Attribute2StringCodeMap: Map<number, number> = new Map([ ...@@ -166,31 +173,31 @@ export const Attribute2StringCodeMap: Map<number, number> = new Map([
]); ]);
// 种族 // 种族
const RACE_WARRIOR = 0x1; // export const RACE_WARRIOR = 0x1; //
const RACE_SPELLCASTER = 0x2; // export const RACE_SPELLCASTER = 0x2; //
const RACE_FAIRY = 0x4; // export const RACE_FAIRY = 0x4; //
const RACE_FIEND = 0x8; // export const RACE_FIEND = 0x8; //
const RACE_ZOMBIE = 0x10; // export const RACE_ZOMBIE = 0x10; //
const RACE_MACHINE = 0x20; // export const RACE_MACHINE = 0x20; //
const RACE_AQUA = 0x40; // export const RACE_AQUA = 0x40; //
const RACE_PYRO = 0x80; // export const RACE_PYRO = 0x80; //
const RACE_ROCK = 0x100; // export const RACE_ROCK = 0x100; //
const RACE_WINDBEAST = 0x200; // export const RACE_WINDBEAST = 0x200; //
const RACE_PLANT = 0x400; // export const RACE_PLANT = 0x400; //
const RACE_INSECT = 0x800; // export const RACE_INSECT = 0x800; //
const RACE_THUNDER = 0x1000; // export const RACE_THUNDER = 0x1000; //
const RACE_DRAGON = 0x2000; // export const RACE_DRAGON = 0x2000; //
const RACE_BEAST = 0x4000; // export const RACE_BEAST = 0x4000; //
const RACE_BEASTWARRIOR = 0x8000; // export const RACE_BEASTWARRIOR = 0x8000; //
const RACE_DINOSAUR = 0x10000; // export const RACE_DINOSAUR = 0x10000; //
const RACE_FISH = 0x20000; // export const RACE_FISH = 0x20000; //
const RACE_SEASERPENT = 0x40000; // export const RACE_SEASERPENT = 0x40000; //
const RACE_REPTILE = 0x80000; // export const RACE_REPTILE = 0x80000; //
const RACE_PSYCHO = 0x100000; // export const RACE_PSYCHO = 0x100000; //
const RACE_DEVINE = 0x200000; // export const RACE_DEVINE = 0x200000; //
const RACE_CREATORGOD = 0x400000; // export const RACE_CREATORGOD = 0x400000; //
const RACE_WYRM = 0x800000; // export const RACE_WYRM = 0x800000; //
const RACE_CYBERSE = 0x1000000; // export const RACE_CYBERSE = 0x1000000; //
export const Race2StringCodeMap: Map<number, number> = new Map([ export const Race2StringCodeMap: Map<number, number> = new Map([
[RACE_WARRIOR, 1020], [RACE_WARRIOR, 1020],
......
...@@ -12,61 +12,29 @@ import { ...@@ -12,61 +12,29 @@ import {
} from "@/api"; } from "@/api";
import { predictDuel } from "@/api/ygoAgent/predict"; import { predictDuel } from "@/api/ygoAgent/predict";
import { import {
convertActionMsg,
convertCard,
convertDeckCard,
convertPhase,
Global, Global,
Input, Input,
MsgSelectSum, MsgSelectSum,
MultiSelectMsg, MultiSelectMsg,
parsePlayerFromMsg,
} from "@/api/ygoAgent/schema"; } from "@/api/ygoAgent/schema";
import {
convertActionMsg,
convertCard,
convertDeckCard,
convertPhase,
convertPositionResponse,
parsePlayerFromMsg,
} from "@/api/ygoAgent/transaction";
import { cardStore, matStore } from "@/stores"; import { cardStore, matStore } from "@/stores";
function computeSetDifference(a1: number[], a2: number[]): number[] { import { argmax, computeSetDifference } from "./util";
const freq1 = new Map<number, number>();
const freq2 = new Map<number, number>();
for (const num of a1) {
freq1.set(num, (freq1.get(num) || 0) + 1);
}
for (const num of a2) {
freq2.set(num, (freq2.get(num) || 0) + 1);
}
for (const [num, count] of freq2) {
if (freq1.has(num)) {
freq1.set(num, freq1.get(num)! - count);
}
}
const difference: number[] = [];
for (const [num, count] of freq1) {
if (count > 0) {
difference.push(...Array(count).fill(num));
}
}
return difference; const { DECK, HAND, MZONE, SZONE, GRAVE, REMOVED, EXTRA } = ygopro.CardZone;
}
export function genInput(msg: ygopro.StocGameMessage): Input { export function genAgentInput(msg: ygopro.StocGameMessage): Input {
// 全局信息可以从 `matStore` 里面拿
const mat = matStore; const mat = matStore;
// 卡片信息可以从 `cardStore` 里面拿
// TODO (ygo-agent): TZONE // TODO (ygo-agent): TZONE
const zones = [ const zones = [DECK, HAND, MZONE, SZONE, GRAVE, REMOVED, EXTRA];
ygopro.CardZone.DECK,
ygopro.CardZone.HAND,
ygopro.CardZone.MZONE,
ygopro.CardZone.SZONE,
ygopro.CardZone.GRAVE,
ygopro.CardZone.REMOVED,
ygopro.CardZone.EXTRA,
];
// select_xxx msg 从参数 `msg` 里获取
// 这里已经保证 `msg` 是众多 `select_xxx` msg 中的一个
const player = parsePlayerFromMsg(msg); const player = parsePlayerFromMsg(msg);
const opponent = 1 - player; const opponent = 1 - player;
...@@ -74,10 +42,7 @@ export function genInput(msg: ygopro.StocGameMessage): Input { ...@@ -74,10 +42,7 @@ export function genInput(msg: ygopro.StocGameMessage): Input {
.filter( .filter(
(card) => (card) =>
zones.includes(card.location.zone) && zones.includes(card.location.zone) &&
!( !(card.location.zone === DECK && card.location.controller === player),
card.location.zone === ygopro.CardZone.DECK &&
card.location.controller === player
),
) )
.map((card) => convertCard(card, player)); .map((card) => convertCard(card, player));
...@@ -108,23 +73,25 @@ export function genInput(msg: ygopro.StocGameMessage): Input { ...@@ -108,23 +73,25 @@ export function genInput(msg: ygopro.StocGameMessage): Input {
const actionMsg = convertActionMsg(msg); const actionMsg = convertActionMsg(msg);
return { return {
global: global, global,
cards: deckCardsMe.concat(cards), cards: deckCardsMe.concat(cards),
action_msg: actionMsg, action_msg: actionMsg,
}; };
} }
async function sendRequest(req: PredictReq) { async function sendRequest(req: PredictReq) {
console.log("Sending predict request:", req);
const duelId = matStore.duelId; const duelId = matStore.duelId;
const resp = await predictDuel(duelId, req); const resp = await predictDuel(duelId, req);
console.log("Got predict response:", resp);
if (resp !== undefined) { if (resp !== undefined) {
matStore.agentIndex = resp.index; matStore.agentIndex = resp.index;
} else { } else {
throw new Error("Failed to get predict response"); throw new Error("Failed to get predict response");
} }
// TODO: 下面的逻辑需要封装一下,因为:
// 1. 现在实现的功能是AI托管,UI上不需要感知AI的预测结果;
// 2. 后面如果需要实现AI辅助功能,UI上需要感知AI的预测结果,
// 所以需要单独提供接口能力。
const preds = resp.predict_results.action_preds; const preds = resp.predict_results.action_preds;
const actionIdx = argmax(preds, (r) => r.prob); const actionIdx = argmax(preds, (r) => r.prob);
matStore.prevActionIndex = actionIdx; matStore.prevActionIndex = actionIdx;
...@@ -132,8 +99,12 @@ async function sendRequest(req: PredictReq) { ...@@ -132,8 +99,12 @@ async function sendRequest(req: PredictReq) {
return pred; return pred;
} }
// TODO:
// 1. 逻辑需要拆分下
// 2. 这个函数在外面被各个 service 模块分散调用,
// 需要改成在`gameMsg.ts`调用,并通过`try..catch`正确处理错误。
export async function sendAIPredictAsResponse(msg: ygopro.StocGameMessage) { export async function sendAIPredictAsResponse(msg: ygopro.StocGameMessage) {
const input = genInput(msg); const input = genAgentInput(msg);
const msgName = input.action_msg.data.msg_type; const msgName = input.action_msg.data.msg_type;
const multiSelectMsgs = ["select_card", "select_tribute", "select_sum"]; const multiSelectMsgs = ["select_card", "select_tribute", "select_sum"];
...@@ -240,37 +211,3 @@ export async function sendAIPredictAsResponse(msg: ygopro.StocGameMessage) { ...@@ -240,37 +211,3 @@ export async function sendAIPredictAsResponse(msg: ygopro.StocGameMessage) {
} }
} }
} }
function argmax<T>(arr: T[], getValue: (item: T) => number): number {
if (arr.length === 0) {
throw new Error("Array is empty");
}
let maxIndex = 0;
let maxValue = getValue(arr[0]);
for (let i = 1; i < arr.length; i++) {
const currentValue = getValue(arr[i]);
if (currentValue > maxValue) {
maxValue = currentValue;
maxIndex = i;
}
}
return maxIndex;
}
function convertPositionResponse(response: number): ygopro.CardPosition {
switch (response) {
case 0x1:
return ygopro.CardPosition.FACEUP_ATTACK;
case 0x2:
return ygopro.CardPosition.FACEDOWN_ATTACK;
case 0x4:
return ygopro.CardPosition.FACEUP_DEFENSE;
case 0x8:
return ygopro.CardPosition.FACEDOWN_DEFENSE;
default:
throw new Error(`Invalid position response: ${response}`);
}
}
// Ygo Agent with AI-Assisted function on Yu-Gi-Oh! Game
export class YgoAgent {
// TODO
}
...@@ -7,6 +7,7 @@ import { displayAnnounceModal } from "@/ui/Duel/Message/AnnounceModal"; ...@@ -7,6 +7,7 @@ import { displayAnnounceModal } from "@/ui/Duel/Message/AnnounceModal";
export default async (announce: MsgAnnounce) => { export default async (announce: MsgAnnounce) => {
if (matStore.autoSelect) { if (matStore.autoSelect) {
// TODO: 如果是开启 AI 模式,不应该调用这个函数
console.log("intercept announce"); console.log("intercept announce");
await sendAIPredictAsResponse( await sendAIPredictAsResponse(
announce as unknown as ygopro.StocGameMessage, announce as unknown as ygopro.StocGameMessage,
......
...@@ -68,6 +68,7 @@ export default async (selectChain: MsgSelectChain) => { ...@@ -68,6 +68,7 @@ export default async (selectChain: MsgSelectChain) => {
case 2: // 处理多张 case 2: // 处理多张
case 3: { case 3: {
if (matStore.autoSelect) { if (matStore.autoSelect) {
// TODO: 确认AI模型是否可以处理其他case的情况
console.log("intercept selectChain"); console.log("intercept selectChain");
await sendAIPredictAsResponse( await sendAIPredictAsResponse(
selectChain as unknown as ygopro.StocGameMessage, selectChain as unknown as ygopro.StocGameMessage,
......
...@@ -8,3 +8,48 @@ export function isAllOnField(locations: ygopro.CardLocation[]): boolean { ...@@ -8,3 +8,48 @@ export function isAllOnField(locations: ygopro.CardLocation[]): boolean {
return locations.find((location) => !isOnField(location)) === undefined; return locations.find((location) => !isOnField(location)) === undefined;
} }
export function computeSetDifference(set1: number[], set2: number[]): number[] {
const freq1 = new Map<number, number>();
const freq2 = new Map<number, number>();
for (const num of set1) {
freq1.set(num, (freq1.get(num) || 0) + 1);
}
for (const num of set2) {
freq2.set(num, (freq2.get(num) || 0) + 1);
}
for (const [num, count] of freq2) {
if (freq1.has(num)) {
freq1.set(num, freq1.get(num)! - count);
}
}
const difference: number[] = [];
for (const [num, count] of freq1) {
if (count > 0) {
difference.push(...Array(count).fill(num));
}
}
return difference;
}
export function argmax<T>(arr: T[], getValue: (item: T) => number): number {
if (arr.length === 0) {
throw new Error("Array is empty");
}
let maxIndex = 0;
let maxValue = getValue(arr[0]);
for (let i = 1; i < arr.length; i++) {
const currentValue = getValue(arr[i]);
if (currentValue > maxValue) {
maxValue = currentValue;
maxIndex = i;
}
}
return maxIndex;
}
...@@ -50,6 +50,7 @@ export interface MatState { ...@@ -50,6 +50,7 @@ export interface MatState {
/** 根据自己的先后手判断是否是自己 */ /** 根据自己的先后手判断是否是自己 */
isMe: (player: number) => boolean; isMe: (player: number) => boolean;
// 下面其中一些貌似可以封装成为`AgentInfo`
turnCount: number; turnCount: number;
duelId: string; duelId: string;
agentIndex: number; agentIndex: number;
......
...@@ -35,7 +35,7 @@ export const NeosModal: React.FC<ModalProps> = (props) => { ...@@ -35,7 +35,7 @@ export const NeosModal: React.FC<ModalProps> = (props) => {
maskClosable={true} maskClosable={true}
onCancel={() => setMini(!mini)} onCancel={() => setMini(!mini)}
closeIcon={mini ? <UpOutlined /> : <MinusOutlined />} closeIcon={mini ? <UpOutlined /> : <MinusOutlined />}
bodyStyle={{ padding: "10px 0" }} style={{ padding: "10px 0" }}
mask={!mini} mask={!mini}
wrapClassName={classNames({ [styles.wrap]: mini })} wrapClassName={classNames({ [styles.wrap]: mini })}
closable={true} closable={true}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment