Commit 2d1ca98c authored by biluo.shen's avatar biluo.shen

Add AI predict

parent d27fb2f1
Pipeline #28077 failed with stages
in 32 seconds
...@@ -3,9 +3,13 @@ ...@@ -3,9 +3,13 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.3.7",
"@ant-design/pro-components": "^2.6.12", "@ant-design/pro-components": "^2.6.12",
"@ant-design/pro-provider": "^2.14.7",
"@dnd-kit/core": "^6.0.8", "@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2", "@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.2",
"@react-spring/shared": "^9.7.3",
"@react-spring/web": "^9.7.3", "@react-spring/web": "^9.7.3",
"antd": "^5.8.3", "antd": "^5.8.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
...@@ -16,6 +20,7 @@ ...@@ -16,6 +20,7 @@
"i18next": "^23.11.4", "i18next": "^23.11.4",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"overlayscrollbars": "^2.9.1",
"overlayscrollbars-react": "^0.5.1", "overlayscrollbars-react": "^0.5.1",
"rdndmb-html5-to-touch": "^8.0.3", "rdndmb-html5-to-touch": "^8.0.3",
"react": "^18.2.0", "react": "^18.2.0",
......
...@@ -5,7 +5,7 @@ import { Input, MsgResponse } from "./schema"; ...@@ -5,7 +5,7 @@ import { Input, MsgResponse } from "./schema";
import { agentHeader } from "./util"; import { agentHeader } from "./util";
const { agentServer } = useConfig(); const { agentServer } = useConfig();
const apiPath = (duelId: string) => `${agentServer}/v0/duels/${duelId}/predict`; const apiPath = (duelId: string) => `v0/duels/${duelId}/predict`;
export interface PredictReq { export interface PredictReq {
/** /**
...@@ -13,6 +13,7 @@ export interface PredictReq { ...@@ -13,6 +13,7 @@ export interface PredictReq {
*/ */
index: number; index: number;
input: Input; input: Input;
prev_action_idx: number;
} }
interface PredictResp { interface PredictResp {
...@@ -27,7 +28,10 @@ export async function predictDuel( ...@@ -27,7 +28,10 @@ export async function predictDuel(
duelId: string, duelId: string,
req: PredictReq, req: PredictReq,
): Promise<PredictResp | undefined> { ): Promise<PredictResp | undefined> {
const headers = agentHeader(); const headers = {
...agentHeader(),
'Content-Type': 'application/json',
};
const resp = await fetch(`${agentServer}/${apiPath(duelId)}`, { const resp = await fetch(`${agentServer}/${apiPath(duelId)}`, {
method: "POST", method: "POST",
......
/* Generated from https://1ewzpubatn.apifox.cn/api-178477939 */ import { ygopro } from "@/api";
export interface Input { import { CardType } from "@/stores/cardStore";
action_msg: ActionMsg; import { extraCardTypes } from "@/common";
cards: Card[];
global: Global;
}
export interface ActionMsg { import GM = ygopro.StocGameMessage;
data: Data;
name: Name;
}
/** /**
* MsgSelectCard * none for N/A or unknown or token.
*
* MsgSelectTribute
*
* MsgSelectSum
*
* MsgSelectIdleCmd
*
* MsgSelectChain
*
* MsgSelectPosition
*
* MsgSelectEffectYn
*
* MsgSelectYesNo
*
* MsgSelectBattleCmd
*
* MsgSelectUnselectCard
*
* MsgSelectOption
*
* MsgSelectPlace
*
* MsgSelectDisfield
*
* MsgAnnounceAttrib
*
* MsgAnnounceNumber
*/
export interface Data {
/**
* ignored
*/
cancelable?: boolean;
cards?: CardElement[];
max?: number;
min?: number;
/**
* the indices of the selected `cards`
*/
selected?: number[];
level_sum?: number;
/**
* size > 2 not supported
*/
must_cards?: MustCard[];
/**
* true not supported
*/
overflow?: boolean;
idle_cmds?: IdleCmd[];
chains?: Chain[];
forced?: boolean;
code?: number;
positions?: PositionElement[];
effect_description?: number;
location?: LocationObject;
battle_cmds?: BattleCmd[];
finishable?: boolean;
selectable_cards?: LocationObject[];
/**
* ignored
*/
selected_cards?: LocationObject[];
/**
* ignored
*/
options?: Option[];
/**
* > 1 not supported; 0 is considered as 1.
*
* != 1 not supported
*/ */
count?: number; enum Attribute {
places?: Place[]; None = "none",
attributes?: AttributeElement[];
numbers?: number[];
[property: string]: any;
}
export enum AttributeElement {
Dark = "dark",
Divine = "divine",
Earth = "earth", Earth = "earth",
Fire = "fire",
Light = "light",
Water = "water", Water = "water",
Fire = "fire",
Wind = "wind", Wind = "wind",
Light = "light",
Dark = "dark",
Divine = "divine",
} }
export interface BattleCmd { // from common.ts ATTRIBUTE_*
cmd_type: BattleCmdCmdType; // TODO (ygo-agent): replace literal numbers with constants
data?: null | PurpleData; function numberToAttribute(attributeNumber: number): Attribute {
} switch (attributeNumber) {
case 0x00:
export enum BattleCmdCmdType { return Attribute.None;
Activate = "activate", case 0x01:
Attack = "attack", return Attribute.Earth;
ToEp = "to_ep", case 0x02:
ToM2 = "to_m2", return Attribute.Water;
} case 0x04:
return Attribute.Fire;
export interface PurpleData { case 0x08:
card_info: CardInfo; return Attribute.Wind;
direct_attackable: boolean; case 0x10:
effect_description: number; return Attribute.Light;
} case 0x20:
return Attribute.Dark;
/** case 0x40:
* CardInfo return Attribute.Divine;
*/ default:
export interface CardInfo { throw new Error(`Unknown attribute number: ${attributeNumber}`);
code: number; }
controller: Controller;
location: LocationEnum;
/**
* Start from 0
*/
sequence: number;
} }
export enum Controller { enum Controller {
Me = "me", Me = "me",
Opponent = "opponent", Opponent = "opponent",
} }
export enum LocationEnum { //
enum Location {
Deck = "deck", Deck = "deck",
Extra = "extra", Extra = "extra",
Grave = "grave", Grave = "grave",
Hand = "hand", Hand = "hand",
Mzone = "mzone", MZone = "mzone",
Removed = "removed", Removed = "removed",
Szone = "szone", SZone = "szone",
} }
/** function cardZoneToLocation(zone: ygopro.CardZone): Location {
* CardLocation switch (zone) {
*/ case ygopro.CardZone.DECK:
export interface CardElement { return Location.Deck;
controller?: Controller; case ygopro.CardZone.HAND:
location: LocationEnum | LocationObject; return Location.Hand;
/** case ygopro.CardZone.MZONE:
* if is overlay, this is the overlay index, starting from 0, else -1. return Location.MZone;
*/ case ygopro.CardZone.SZONE:
overlay_sequence?: number; return Location.SZone;
/** case ygopro.CardZone.GRAVE:
* Start from 0 return Location.Grave;
*/ case ygopro.CardZone.REMOVED:
sequence?: number; return Location.Removed;
level?: number; case ygopro.CardZone.EXTRA:
level1?: number; return Location.Extra;
/** default:
* ignored throw new Error(`Unknown card zone: ${zone}`);
*/ }
level2?: number;
} }
/** interface Place {
* CardLocation
*/
export interface LocationObject {
controller: Controller; controller: Controller;
location: LocationEnum; location: Location;
/**
* if is overlay, this is the overlay index, starting from 0, else -1.
*/
overlay_sequence: number;
/** /**
* Start from 0 * Start from 0
*/ */
sequence: number; sequence: number;
} }
export interface Chain { interface Option {
code: number; code: number;
effect_description: number;
location: LocationObject;
}
export interface IdleCmd {
cmd_type: IdleCmdCmdType;
data?: null | FluffyData;
}
export enum IdleCmdCmdType {
Activate = "activate",
Mset = "mset",
Reposition = "reposition",
Set = "set",
SpSummon = "sp_summon",
Summon = "summon",
ToBp = "to_bp",
ToEp = "to_ep",
}
export interface FluffyData {
card_info: CardInfo;
effect_description: number;
} }
export interface MustCard { interface CardLocation {
level1: number; controller: Controller;
location: Location;
/** /**
* ignored * if is overlay, this is the overlay index, starting from 0, else -1.
*/ */
level2: number; overlay_sequence: number;
location: LocationObject;
}
export interface Option {
code: number;
}
export interface Place {
controller: Controller;
location: LocationEnum;
/** /**
* Start from 0 * Start from 0
*/ */
sequence: number; sequence: number;
} }
export enum PositionElement { function convertController(controller: number, player: number): Controller {
Attack = "attack", return controller === player ? Controller.Me : Controller.Opponent;
Defense = "defense",
Facedown = "facedown",
FacedownAttack = "facedown_attack",
FacedownDefense = "facedown_defense",
Faceup = "faceup",
FaceupAttack = "faceup_attack",
FaceupDefense = "faceup_defense",
} }
export enum Name { function convertOverlaySequence(cl: ygopro.CardLocation): number {
AnnounceAttrib = "announce_attrib", return cl.is_overlay ? cl.overlay_sequence : -1;
AnnounceNumber = "announce_number",
SelectBattlecmd = "select_battlecmd",
SelectCard = "select_card",
SelectChain = "select_chain",
SelectDisfield = "select_disfield",
SelectEffectyn = "select_effectyn",
SelectIdlecmd = "select_idlecmd",
SelectOption = "select_option",
SelectPlace = "select_place",
SelectPosition = "select_position",
SelectSum = "select_sum",
SelectTribute = "select_tribute",
SelectUnselectCard = "select_unselect_card",
SelectYesno = "select_yesno",
}
/**
* Card
*/
export interface Card {
attack: number;
/**
* none for N/A or unknown or token.
*/
attribute: CardAttribute;
/**
* Card code from cards.cdb
*/
code: number;
controller: Controller;
/**
* Number of counters. If there are 2 types of counters or more, we consider only the first
* type of counter.
*/
counter: number;
defense: number;
/**
* Rank and link are also considered as level. 0 is N/A or unknown.
*/
level: number;
location: LocationEnum;
/**
* Whether the card effect is disabled or forbidden
*/
negated: boolean;
/**
* if is overlay, this is the overlay index, starting from 0, else -1.
*/
overlay_sequence: number;
/**
* If the monster is xyz material (overlay_sequence != -1), the position is faceup.
*/
position: CardPosition;
/**
* none for N/A or unknown or token.
*/
race: Race;
/**
* Sequence in ocgcore, 0 is N/A or unknown, if not, shoud start from 1. Only non-zero for
* cards in mzone, szone and grave.
*/
sequence: number;
types: Type[];
} }
/** function convertCardLocation(cl: ygopro.CardLocation, player: number): CardLocation {
* none for N/A or unknown or token. return {
*/ controller: convertController(cl.controller, player),
export enum CardAttribute { location: cardZoneToLocation(cl.zone),
Dark = "dark", overlay_sequence: convertOverlaySequence(cl),
Divine = "divine", sequence: cl.sequence,
Earth = "earth", };
Fire = "fire",
Light = "light",
None = "none",
Water = "water",
Wind = "wind",
} }
/** /**
* 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.
*/ */
export enum CardPosition { enum Position {
Attack = "attack", None = "none",
Defense = "defense",
Facedown = "facedown",
FacedownAttack = "facedown_attack",
FacedownDefense = "facedown_defense",
Faceup = "faceup",
FaceupAttack = "faceup_attack", FaceupAttack = "faceup_attack",
FacedownAttack = "facedown_attack",
Attack = "attack",
FaceupDefense = "faceup_defense", FaceupDefense = "faceup_defense",
None = "none", Faceup = "faceup",
FacedownDefense = "facedown_defense",
Facedown = "facedown",
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.
*/ */
export enum Race { enum Race {
Aqua = "aqua", Aqua = "aqua",
Beast = "beast", Beast = "beast",
BeastWarrior = "beast_warrior", BeastWarrior = "beast_warrior",
...@@ -375,7 +196,42 @@ export enum Race { ...@@ -375,7 +196,42 @@ export enum Race {
Zombie = "zombie", Zombie = "zombie",
} }
export enum Type { // from common.ts RACE_*
// 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",
...@@ -403,22 +259,119 @@ export enum Type { ...@@ -403,22 +259,119 @@ export enum Type {
Xyz = "xyz", Xyz = "xyz",
} }
/** // from common.ts TYPE_*
* Global // 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
*/ */
export interface Global { code: number;
location: Location;
/** /**
* Whether me is the first player * Sequence in ocgcore, 0 is N/A or unknown, if not, shoud start from 1. Only non-zero for
* cards in mzone, szone and grave.
*/ */
is_first: boolean; sequence: number;
is_my_turn: boolean; controller: Controller;
my_lp: number; /**
op_lp: number; * If the monster is xyz material (overlay_sequence != -1), the position is faceup.
phase: Phase; */
turn: number; position: Position;
/**
* if is overlay, this is the overlay index, starting from 0, else -1.
*/
overlay_sequence: number;
/**
* none for N/A or unknown or token.
*/
attribute: Attribute;
/**
* none for N/A or unknown or token.
*/
race: Race;
/**
* Rank and link are also considered as level. 0 is N/A or unknown.
*/
level: number;
/**
* Number of counters. If there are 2 types of counters or more, we consider only the first
* type of counter.
*/
counter: number;
/**
* Whether the card effect is disabled or forbidden
*/
negated: boolean;
attack: number;
defense: number;
types: Type[];
}
function getCounter(counters: { [type: number]: number }) {
if (counters) {
for (const type in counters) {
return counters[type];
}
}
return 0;
}
export function convertCard(card: CardType, player: number): Card {
// TODO (ygo-agent): unseen cards
return {
code: card.code,
location: cardZoneToLocation(card.location.zone),
sequence: card.location.sequence + 1,
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),
}
} }
export enum Phase { enum Phase {
Battle = "battle", Battle = "battle",
BattleStart = "battle_start", BattleStart = "battle_start",
BattleStep = "battle_step", BattleStep = "battle_step",
...@@ -431,6 +384,578 @@ export enum Phase { ...@@ -431,6 +384,578 @@ export 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 {
/**
* Whether me is the first player
*/
is_first: boolean;
is_my_turn: boolean;
my_lp: number;
op_lp: number;
phase: Phase;
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 MsgSelectCard {
cancelable: boolean;
min: number;
max: number;
cards: CardLocation[];
selected: number[];
}
function convertMsgSelectCard(msg: GM.MsgSelectCard): MsgSelectCard {
return {
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
cards: msg.cards.map(c => convertCardLocation(c.location, msg.player)),
// TODO (ygo-agent): the indices of the selected `cards`
selected: [],
};
}
interface SelectTributeCard {
location: CardLocation;
level: number;
}
interface MsgSelectTribute {
cancelable: boolean;
min: number;
max: number;
cards: SelectTributeCard[];
selected: number[];
}
function convertMsgSelectTribute(msg: GM.MsgSelectTribute): MsgSelectTribute {
return {
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
cards: msg.selectable_cards.map(c => ({
location: convertCardLocation(c.location, msg.player),
level: c.level,
})),
// TODO (ygo-agent): the indices of the selected `cards`
selected: [],
};
}
interface SelectSumCard {
location: CardLocation;
level1: number;
level2: number;
}
interface MsgSelectSum {
overflow: boolean;
level_sum: number;
min: number;
max: number;
cards: SelectSumCard[];
must_cards: SelectSumCard[];
selected: number[];
}
function convertMsgSelectSum(msg: GM.MsgSelectSum): MsgSelectSum {
return {
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,
})),
must_cards: msg.must_select_cards.map(c => ({
location: convertCardLocation(c.location, msg.player),
level1: c.level1,
level2: c.level2,
})),
// TODO (ygo-agent): the indices of the selected `cards`
selected: [],
}
}
interface CardInfo {
code: number;
controller: Controller;
location: Location;
sequence: number;
}
function convertCardInfo(cardInfo: ygopro.CardInfo, player: number): CardInfo {
return {
code: cardInfo.code,
controller: convertController(cardInfo.controller, player),
location: cardZoneToLocation(cardInfo.location),
sequence: cardInfo.sequence,
};
}
enum IdleCmdType {
Summon = "summon",
SpSummon = "sp_summon",
Reposition = "reposition",
Mset = "mset",
Set = "set",
Activate = "activate",
ToBp = "to_bp",
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 {
card_info: CardInfo;
effect_description: number;
}
interface IdleCmd {
cmd_type: IdleCmdType;
data?: IdleCmdData;
}
interface MsgSelectIdleCmd {
idle_cmds: IdleCmd[];
}
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,
},
});
} else {
throw new Error(`Unsupported idle command type: ${cmd_type}`);
}
}
}
if (msg.enable_bp) {
idle_cmds.push({ cmd_type: IdleCmdType.ToBp });
}
if (msg.enable_ep) {
idle_cmds.push({ cmd_type: IdleCmdType.ToEp });
}
return { idle_cmds };
}
interface Chain {
code: number;
location: CardLocation;
effect_description: number;
}
interface MsgSelectChain {
forced: boolean;
chains: Chain[];
}
function convertChain(chain: GM.MsgSelectChain.Chain, player: number): Chain {
return {
code: chain.code,
location: convertCardLocation(chain.location, player),
effect_description: chain.effect_description,
};
}
function convertMsgSelectChain(msg: GM.MsgSelectChain): MsgSelectChain {
return {
forced: msg.forced,
chains: msg.chains.map(c => convertChain(c, msg.player)),
};
}
interface MsgSelectPosition {
code: number;
positions: Position[];
}
function convertMsgSelectPosition(msg: GM.MsgSelectPosition): MsgSelectPosition {
return {
code: msg.code,
positions: msg.positions.map(p => convertPosition(p.position)),
};
}
interface MsgSelectYesNo {
effect_description: number;
}
function convertMsgSelectYesNo(msg: GM.MsgSelectYesNo): MsgSelectYesNo {
return {
effect_description: msg.effect_description,
};
}
interface MsgSelectEffectYn {
code: number;
location: CardLocation;
effect_description: number;
}
function convertMsgSelectEffectYn(msg: GM.MsgSelectEffectYn): MsgSelectEffectYn {
return {
code: msg.code,
location: convertCardLocation(msg.location, msg.player),
effect_description: msg.effect_description,
};
}
enum BattleCmdType {
Attack = "attack",
Activate = "activate",
ToM2 = "to_m2",
ToEp = "to_ep",
}
import _BattleCmdType = GM.MsgSelectBattleCmd.BattleCmd.BattleType;
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;
effect_description: number;
direct_attackable: boolean;
}
interface BattleCmd {
cmd_type: BattleCmdType;
data?: BattleCmdData;
}
interface MsgSelectBattleCmd {
battle_cmds: BattleCmd[];
}
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,
};
battle_cmds.push({ cmd_type, data: battle_data });
}
}
if (msg.enable_m2) {
battle_cmds.push({ cmd_type: BattleCmdType.ToM2 });
}
if (msg.enable_ep) {
battle_cmds.push({ cmd_type: BattleCmdType.ToEp });
}
return { battle_cmds };
}
interface MsgSelectUnselectCard {
finishable: boolean;
cancelable: boolean;
min: number;
max: number;
selected_cards: CardLocation[];
selectable_cards: CardLocation[];
}
function convertMsgSelectUnselectCard(msg: GM.MsgSelectUnselectCard): MsgSelectUnselectCard {
return {
finishable: msg.finishable,
cancelable: msg.cancelable,
min: msg.min,
max: msg.max,
selected_cards: msg.selected_cards.map(c => convertCardLocation(c.location, msg.player)),
selectable_cards: msg.selectable_cards.map(c => convertCardLocation(c.location, msg.player)),
};
}
interface Option {
code: number;
}
interface MsgSelectOption {
options: Option[];
}
function convertMsgSelectOption(msg: GM.MsgSelectOption): MsgSelectOption {
return {
options: msg.options.map(o => ({ code: o.code })),
};
}
interface Place {
controller: Controller;
location: Location;
sequence: number;
}
interface MsgSelectPlace {
count: number;
places: Place[];
}
function convertMsgSelectPlace(msg: GM.MsgSelectPlace): MsgSelectPlace {
return {
count: msg.count,
places: msg.places.map(p => ({
controller: convertController(p.controller, msg.player),
location: cardZoneToLocation(p.zone),
sequence: p.sequence,
})),
};
}
// TODO (ygo-agent): MsgSelectDisfield
interface MsgAnnounceAttrib {
count: number;
attributes: Attribute[];
}
function convertMsgAnnounceAttrib(msg: GM.MsgAnnounce): MsgAnnounceAttrib {
return {
count: msg.min,
// from api/ocgcore/ocgAdapter/stoc/stocGameMsg/announceAttrib.ts
attributes: msg.options.map(a => numberToAttribute(1 << a.code)),
};
}
interface MsgAnnounceNumber {
count: number;
numbers: number[];
}
function convertMsgAnnounceNumber(msg: GM.MsgAnnounce): MsgAnnounceNumber {
return {
count: msg.min,
numbers: msg.options.map(o => o.code),
};
}
type ActionMsgData =
MsgSelectCard |
MsgSelectTribute |
MsgSelectSum |
MsgSelectIdleCmd |
MsgSelectChain |
MsgSelectPosition |
MsgSelectYesNo |
MsgSelectEffectYn |
MsgSelectBattleCmd |
MsgSelectUnselectCard |
MsgSelectOption |
MsgSelectPlace |
MsgAnnounceAttrib |
MsgAnnounceNumber;
enum ActionMsgName {
AnnounceAttrib = "announce_attrib",
AnnounceNumber = "announce_number",
SelectBattlecmd = "select_battlecmd",
SelectCard = "select_card",
SelectChain = "select_chain",
SelectDisfield = "select_disfield",
SelectEffectyn = "select_effectyn",
SelectIdlecmd = "select_idlecmd",
SelectOption = "select_option",
SelectPlace = "select_place",
SelectPosition = "select_position",
SelectSum = "select_sum",
SelectTribute = "select_tribute",
SelectUnselectCard = "select_unselect_card",
SelectYesno = "select_yesno",
}
interface ActionMsg {
data: ActionMsgData;
name: ActionMsgName;
}
export function convertActionMsg(msg: ygopro.StocGameMessage): ActionMsg {
if (msg instanceof GM.MsgSelectCard) {
return {
name: ActionMsgName.SelectCard,
data: convertMsgSelectCard(msg),
};
} else if (msg instanceof GM.MsgSelectTribute) {
return {
name: ActionMsgName.SelectTribute,
data: convertMsgSelectTribute(msg),
};
} else if (msg instanceof GM.MsgSelectSum) {
return {
name: ActionMsgName.SelectSum,
data: convertMsgSelectSum(msg),
};
} else if (msg instanceof GM.MsgSelectIdleCmd) {
return {
name: ActionMsgName.SelectIdlecmd,
data: convertMsgSelectIdleCmd(msg),
};
} else if (msg instanceof GM.MsgSelectChain) {
return {
name: ActionMsgName.SelectChain,
data: convertMsgSelectChain(msg),
};
} else if (msg instanceof GM.MsgSelectPosition) {
return {
name: ActionMsgName.SelectPosition,
data: convertMsgSelectPosition(msg),
};
} else if (msg instanceof GM.MsgSelectEffectYn) {
return {
name: ActionMsgName.SelectEffectyn,
data: convertMsgSelectEffectYn(msg),
};
} else if (msg instanceof GM.MsgSelectYesNo) {
return {
name: ActionMsgName.SelectYesno,
data: convertMsgSelectYesNo(msg),
};
} else if (msg instanceof GM.MsgSelectBattleCmd) {
return {
name: ActionMsgName.SelectBattlecmd,
data: convertMsgSelectBattleCmd(msg),
};
} else if (msg instanceof GM.MsgSelectUnselectCard) {
return {
name: ActionMsgName.SelectUnselectCard,
data: convertMsgSelectUnselectCard(msg),
};
} else if (msg instanceof GM.MsgSelectOption) {
return {
name: ActionMsgName.SelectOption,
data: convertMsgSelectOption(msg),
};
} else if (msg instanceof GM.MsgSelectPlace) {
return {
name: ActionMsgName.SelectPlace,
data: convertMsgSelectPlace(msg),
};
} else if (msg instanceof GM.MsgAnnounce) {
if (msg.announce_type == GM.MsgAnnounce.AnnounceType.Attribute) {
return {
name: ActionMsgName.AnnounceAttrib,
data: convertMsgAnnounceAttrib(msg),
};
} else if (msg.announce_type == GM.MsgAnnounce.AnnounceType.Number) {
return {
name: ActionMsgName.AnnounceNumber,
data: convertMsgAnnounceNumber(msg),
};
} else {
throw new Error(`Unsupported announce type: ${msg.announce_type}`);
}
} else {
throw new Error(`Unsupported message type: ${msg}`);
}
}
export interface Input {
action_msg: ActionMsg;
cards: Card[];
global: Global;
}
/** /**
* MsgResponse * MsgResponse
*/ */
......
import { PredictReq, ygopro } from "@/api"; import { PredictReq, ygopro } from "@/api";
import { cardStore, matStore, placeStore } from "@/stores"; import { cardStore, matStore } from "@/stores";
import { Global, convertPhase, convertCard, parsePlayerFromMsg, convertActionMsg, Input } from "@/api/ygoAgent/schema";
export function genPredictReq(msg: ygopro.StocGameMessage): PredictReq { export function genPredictReq(msg: ygopro.StocGameMessage): PredictReq {
// 全局信息可以从 `matStore` 里面拿 // 全局信息可以从 `matStore` 里面拿
const mat = matStore; const mat = matStore;
// 卡片信息可以从 `cardStore` 里面拿 // 卡片信息可以从 `cardStore` 里面拿
const cards = cardStore.inner; const zones = [
// 选择场上位置的可选项可以从 `cardStore` 里面拿 ygopro.CardZone.DECK,
// 也可以从 `selectPlace` msg 里面获取 ygopro.CardZone.HAND,
const place = placeStore; ygopro.CardZone.MZONE,
ygopro.CardZone.SZONE,
ygopro.CardZone.GRAVE,
ygopro.CardZone.REMOVED,
ygopro.CardZone.EXTRA,
];
// select_xxx msg 从参数 `msg` 里获取 // select_xxx msg 从参数 `msg` 里获取
// 这里已经保证 `msg` 是众多 `select_xxx` msg 中的一个 // 这里已经保证 `msg` 是众多 `select_xxx` msg 中的一个
const duelId = mat.duelId; const player = parsePlayerFromMsg(msg);
const index = mat.agentIndex; // TODO (ygo-agent): check if this is correct
const opponent = 1 - player;
const cards = cardStore.inner
.filter((card) => zones.includes(card.location.zone))
.map((card) => convertCard(card, player));
const turnPlayer = mat.currentPlayer;
const global: Global = {
is_first: player === 0,
is_my_turn: turnPlayer === player,
my_lp: mat.initInfo.of(player).life,
op_lp: mat.initInfo.of(opponent).life,
phase: convertPhase(mat.phase.currentPhase),
// TODO (ygo-agent): use real turn
turn: 1,
}
const actionMsg = convertActionMsg(msg);
const input: Input = {
global: global,
cards: cards,
action_msg: actionMsg,
}
// TODO: impl return {
index: mat.agentIndex,
input: input,
// TODO (ygo-agent): use real value
prev_action_idx: 0,
}
} }
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { replayStore } from "@/stores"; import { matStore, replayStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message"; import { showWaiting } from "@/ui/Duel/Message";
import onAnnounce from "./announce"; import onAnnounce from "./announce";
...@@ -100,6 +100,63 @@ export default async function handleGameMsg( ...@@ -100,6 +100,63 @@ export default async function handleGameMsg(
if (replayStore.isReplay && ReplayIgnoreMsg.includes(msg.gameMsg)) return; if (replayStore.isReplay && ReplayIgnoreMsg.includes(msg.gameMsg)) return;
switch (msg.gameMsg) {
case "select_card": {
matStore.actionMsg = msg.select_card;
break;
}
case "select_tribute": {
matStore.actionMsg = msg.select_tribute;
break;
}
case "select_sum": {
matStore.actionMsg = msg.select_sum;
break;
}
case "select_idle_cmd": {
matStore.actionMsg = msg.select_idle_cmd;
break;
}
case "select_chain": {
matStore.actionMsg = msg.select_chain;
break;
}
case "select_position": {
matStore.actionMsg = msg.select_position;
break;
}
case "select_effect_yn": {
matStore.actionMsg = msg.select_effect_yn;
break;
}
case "select_yes_no": {
matStore.actionMsg = msg.select_yes_no;
break;
}
case "select_battle_cmd": {
matStore.actionMsg = msg.select_battle_cmd;
break;
}
case "select_unselect_card": {
matStore.actionMsg = msg.select_unselect_card;
break;
}
case "select_option": {
matStore.actionMsg = msg.select_option;
break;
}
case "select_place": {
matStore.actionMsg = msg.select_place;
break;
}
case "announce":
matStore.actionMsg = msg.announce;
break;
default: {
break;
}
}
switch (msg.gameMsg) { switch (msg.gameMsg) {
case "start": { case "start": {
await onMsgStart(msg.start); await onMsgStart(msg.start);
......
...@@ -94,7 +94,8 @@ const initialState: Omit<MatState, "reset"> = { ...@@ -94,7 +94,8 @@ const initialState: Omit<MatState, "reset"> = {
// methods // methods
isMe, isMe,
duelId: "", duelId: "",
agentIndex: 0 agentIndex: 0,
actionMsg: undefined
}; };
class MatStore implements MatState, NeosStore { class MatStore implements MatState, NeosStore {
...@@ -113,6 +114,7 @@ class MatStore implements MatState, NeosStore { ...@@ -113,6 +114,7 @@ class MatStore implements MatState, NeosStore {
duelEnd = initialState.duelEnd; duelEnd = initialState.duelEnd;
duelId = initialState.duelId; duelId = initialState.duelId;
agentIndex = initialState.agentIndex; agentIndex = initialState.agentIndex;
actionMsg = initialState.actionMsg;
// methods // methods
isMe = initialState.isMe; isMe = initialState.isMe;
......
...@@ -52,6 +52,7 @@ export interface MatState { ...@@ -52,6 +52,7 @@ export interface MatState {
duelId: string; duelId: string;
agentIndex: number; agentIndex: number;
actionMsg?: any;
} }
export interface InitInfo { export interface InitInfo {
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
CloseCircleFilled, CloseCircleFilled,
MessageFilled, MessageFilled,
StepForwardFilled, StepForwardFilled,
RobotFilled,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { import {
Button, Button,
...@@ -28,6 +29,8 @@ import { ...@@ -28,6 +29,8 @@ import {
} from "@/api"; } from "@/api";
import { ChainSetting, matStore } from "@/stores"; import { ChainSetting, matStore } from "@/stores";
import { IconFont } from "@/ui/Shared"; import { IconFont } from "@/ui/Shared";
import { predictDuel } from "@/api/ygoAgent/predict";
import { genPredictReq } from "@/service/duel/agent";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType; import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
...@@ -297,6 +300,15 @@ export const Menu = () => { ...@@ -297,6 +300,15 @@ export const Menu = () => {
const globalDisable = !matStore.isMe(currentPlayer); const globalDisable = !matStore.isMe(currentPlayer);
const aiPredict = async () => {
console.log("AI Predict");
const req = genPredictReq(matStore.actionMsg);
console.log(req);
const duelId = matStore.duelId;
const predictRes = await predictDuel(duelId, req);
console.log(predictRes);
};
return ( return (
<div className={styles["menu-container"]}> <div className={styles["menu-container"]}>
<SelectManager /> <SelectManager />
...@@ -323,6 +335,13 @@ export const Menu = () => { ...@@ -323,6 +335,13 @@ export const Menu = () => {
type="text" type="text"
></Button> ></Button>
</DropdownWithTitle> </DropdownWithTitle>
<Tooltip title="AI">
<Button
icon={<RobotFilled />}
onClick={aiPredict}
type="text"
></Button>
</Tooltip>
<Tooltip title={i18n("ChatRoom")}> <Tooltip title={i18n("ChatRoom")}>
<Button <Button
icon={<MessageFilled />} icon={<MessageFilled />}
......
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