Commit 677c0a61 authored by Chunchi Che's avatar Chunchi Che

YgoAgent

parent 52da069f
Pipeline #28419 failed with stages
in 2 minutes and 33 seconds
......@@ -388,24 +388,36 @@ export function convertPhase(phase: _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;
}
switch (msg.gameMsg) {
case "select_card":
return msg.select_card.player;
case "select_tribute":
return msg.select_tribute.player;
case "select_sum":
return msg.select_sum.player;
case "select_idle_cmd":
return msg.select_idle_cmd.player;
case "select_chain":
return msg.select_chain.player;
case "select_position":
return msg.select_position.player;
case "select_effect_yn":
return msg.select_effect_yn.player;
case "select_yes_no":
return msg.select_yes_no.player;
case "select_battle_cmd":
return msg.select_battle_cmd.player;
case "select_unselect_card":
return msg.select_unselect_card.player;
case "select_option":
return msg.select_option.player;
case "select_place":
return msg.select_place.player;
case "announce":
return msg.announce.player;
default:
throw new Error(`Unsupported message type: ${msg}`);
}
}
function convertMsgSelectCard(msg: GM.MsgSelectCard): MsgSelectCard {
......@@ -697,67 +709,50 @@ function convertMsgAnnounceNumber(msg: GM.MsgAnnounce): MsgAnnounceNumber {
}
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) {
switch (msg.gameMsg) {
case "select_card":
return {
data: convertMsgSelectCard(msg.select_card),
};
case "select_tribute":
return { data: convertMsgSelectTribute(msg.select_tribute) };
case "select_sum":
return { data: convertMsgSelectSum(msg.select_sum) };
case "select_idle_cmd":
return { data: convertMsgSelectIdleCmd(msg.select_idle_cmd) };
case "select_chain":
return { data: convertMsgSelectChain(msg.select_chain) };
case "select_position":
return { data: convertMsgSelectPosition(msg.select_position) };
case "select_effect_yn":
return { data: convertMsgSelectEffectYn(msg.select_effect_yn) };
case "select_yes_no":
return { data: convertMsgSelectYesNo(msg.select_yes_no) };
case "select_battle_cmd":
return { data: convertMsgSelectBattleCmd(msg.select_battle_cmd) };
case "select_unselect_card":
return { data: convertMsgSelectUnselectCard(msg.select_unselect_card) };
case "select_option":
return { data: convertMsgSelectOption(msg.select_option) };
case "select_place":
return { data: convertMsgSelectPlace(msg.select_place) };
case "announce": {
const announce = msg.announce;
if (announce.announce_type === GM.MsgAnnounce.AnnounceType.Attribute) {
return {
data: convertMsgAnnounceAttrib(announce),
};
} else if (
announce.announce_type === GM.MsgAnnounce.AnnounceType.Number
) {
return {
data: convertMsgAnnounceNumber(msg),
data: convertMsgAnnounceNumber(announce),
};
} else {
throw new Error(`Unsupported announce type: ${msg.announce_type}`);
throw new Error(`Unsupported announce type: ${announce.announce_type}`);
}
} else {
}
default:
throw new Error(`Unsupported message type: ${msg}`);
}
}
......
......@@ -6,6 +6,7 @@ import {
MatStore,
PlaceStore,
RoomStore,
SideStore,
} from "@/stores";
interface ContextInitInfo {
......@@ -14,6 +15,7 @@ interface ContextInitInfo {
placeStore?: PlaceStore;
roomStore?: RoomStore;
chatStore?: ChatStore;
sideStore?: SideStore;
}
export class Context {
......@@ -22,16 +24,18 @@ export class Context {
public placeStore: PlaceStore;
public roomStore: RoomStore;
public chatStore: ChatStore;
public sideStore: SideStore;
constructor();
constructor(initInfo: ContextInitInfo);
constructor(initInfo?: ContextInitInfo) {
const { matStore, cardStore, placeStore, roomStore, chatStore } =
const { matStore, cardStore, placeStore, roomStore, chatStore, sideStore } =
initInfo ?? {};
this.matStore = matStore ?? new MatStore();
this.cardStore = cardStore ?? new CardStore();
this.placeStore = placeStore ?? new PlaceStore();
this.roomStore = roomStore ?? new RoomStore();
this.chatStore = chatStore ?? new ChatStore();
this.sideStore = sideStore ?? new SideStore();
}
}
......@@ -5,8 +5,20 @@ import { Context } from "./context";
export class Container {
public context: Context;
public conn: WebSocketStream;
// ref: https://yugioh.fandom.com/wiki/Kuriboh
private enableKuriboh: boolean = false;
constructor(context: Context, conn: WebSocketStream) {
this.context = context;
this.conn = conn;
}
public setEnableKuriboh(value: boolean) {
this.enableKuriboh = value;
}
public getEnableKuriboh(): boolean {
return this.enableKuriboh;
}
}
import {
PredictReq,
sendSelectBattleCmdResponse,
sendSelectEffectYnResponse,
sendSelectIdleCmdResponse,
sendSelectMultiResponse,
sendSelectOptionResponse,
sendSelectPlaceResponse,
sendSelectPositionResponse,
sendSelectSingleResponse,
ygopro,
} from "@/api";
import { predictDuel } from "@/api/ygoAgent/predict";
import {
Global,
Input,
MsgSelectSum,
MultiSelectMsg,
} from "@/api/ygoAgent/schema";
import {
convertActionMsg,
convertCard,
convertDeckCard,
convertPhase,
convertPositionResponse,
parsePlayerFromMsg,
} from "@/api/ygoAgent/transaction";
import { Container } from "@/container";
import { cardStore, matStore } from "@/stores";
import { argmax, computeSetDifference } from "./util";
const { DECK, HAND, MZONE, SZONE, GRAVE, REMOVED, EXTRA } = ygopro.CardZone;
export function genAgentInput(msg: ygopro.StocGameMessage): Input {
const mat = matStore;
// TODO (ygo-agent): TZONE
const zones = [DECK, HAND, MZONE, SZONE, GRAVE, REMOVED, EXTRA];
const player = parsePlayerFromMsg(msg);
const opponent = 1 - player;
const cards = cardStore.inner
.filter(
(card) =>
zones.includes(card.location.zone) &&
!(card.location.zone === DECK && card.location.controller === player),
)
.map((card) => convertCard(card, player));
const cardCodesMe = cardStore.inner
.filter(
(card) =>
zones.includes(card.location.zone) &&
card.location.controller === player,
)
.map((card) => card.code);
const cardCodesMeDeck = computeSetDifference(mat.mainDeck, cardCodesMe);
const mainDeckCardMeta = mat.mainDeckCardMeta;
// TODO (ygo-agent): 临时方案,有很多边界情况未考虑
const deckCardsMe = cardCodesMeDeck.map((code) =>
convertDeckCard(mainDeckCardMeta.get(code)!),
);
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),
turn: mat.turnCount,
};
const actionMsg = convertActionMsg(msg);
return {
global,
cards: deckCardsMe.concat(cards),
action_msg: actionMsg,
};
}
async function sendRequest(req: PredictReq) {
const duelId = matStore.duelId;
const resp = await predictDuel(duelId, req);
if (resp !== undefined) {
matStore.agentIndex = resp.index;
} else {
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 actionIdx = argmax(preds, (r) => r.prob);
matStore.prevActionIndex = actionIdx;
const pred = preds[actionIdx];
return pred;
}
// TODO:
// 1. 逻辑需要拆分下
// 2. 这个函数在外面被各个 service 模块分散调用,
// 需要改成在`gameMsg.ts`调用,并通过`try..catch`正确处理错误。
export async function sendAIPredictAsResponse(
container: Container,
msg: ygopro.StocGameMessage,
) {
const conn = container.conn;
const input = genAgentInput(msg);
const msgName = input.action_msg.data.msg_type;
const multiSelectMsgs = ["select_card", "select_tribute", "select_sum"];
if (multiSelectMsgs.includes(msgName)) {
switch (msgName) {
case "select_tribute":
case "select_card": {
const msg_ = input.action_msg.data as MultiSelectMsg;
const selected = [];
const responses = [];
while (true) {
msg_.selected = selected;
const req = {
index: matStore.agentIndex,
input: input,
prev_action_idx: matStore.prevActionIndex,
};
const response = (await sendRequest(req)).response;
if (response !== -1) {
selected.push(matStore.prevActionIndex);
responses.push(response);
}
if (response === -1 || selected.length === msg_.max) {
sendSelectMultiResponse(conn, responses);
break;
}
}
break;
}
case "select_sum":
const msg_ = input.action_msg.data as MsgSelectSum;
const selected = [];
const responses = [];
for (const c of msg_.must_cards) {
responses.push(c.response);
}
while (true) {
msg_.selected = selected;
const req = {
index: matStore.agentIndex,
input: input,
prev_action_idx: matStore.prevActionIndex,
};
const pred = await sendRequest(req);
const idx = matStore.prevActionIndex;
selected.push(idx);
responses.push(pred.response);
if (pred.can_finish) {
sendSelectMultiResponse(conn, responses);
break;
}
}
break;
}
} else {
const req = {
index: matStore.agentIndex,
input: input,
prev_action_idx: matStore.prevActionIndex,
};
const response = (await sendRequest(req)).response;
switch (msgName) {
case "announce_attrib":
case "announce_number":
sendSelectOptionResponse(conn, response);
break;
case "select_battlecmd":
sendSelectBattleCmdResponse(conn, response);
break;
case "select_chain":
sendSelectSingleResponse(conn, response);
break;
case "select_yesno":
case "select_effectyn":
sendSelectEffectYnResponse(conn, response === 1);
break;
case "select_idlecmd":
sendSelectIdleCmdResponse(conn, response);
break;
case "select_option":
sendSelectOptionResponse(conn, response);
break;
case "select_position":
sendSelectPositionResponse(conn, convertPositionResponse(response));
break;
case "select_place": {
const place = (msg as unknown as ygopro.StocGameMessage.MsgSelectPlace)
.places[response];
sendSelectPlaceResponse(conn, {
controller: place.controller,
zone: place.zone,
sequence: place.sequence,
});
break;
}
case "select_unselect_card": {
if (response === -1) {
sendSelectSingleResponse(conn, -1);
} else {
sendSelectMultiResponse(conn, [response]);
}
break;
}
}
}
}
// Ygo Agent with AI-Assisted function on Yu-Gi-Oh! Game
import {
CardMeta,
createDuel,
fetchCard,
PredictReq,
sendSelectBattleCmdResponse,
sendSelectEffectYnResponse,
sendSelectIdleCmdResponse,
sendSelectMultiResponse,
sendSelectOptionResponse,
sendSelectPlaceResponse,
sendSelectPositionResponse,
sendSelectSingleResponse,
sendSortCardResponse,
ygopro,
} from "@/api";
import { predictDuel } from "@/api/ygoAgent/predict";
import {
Global,
Input,
MsgSelectSum,
MultiSelectMsg,
} from "@/api/ygoAgent/schema";
import {
convertActionMsg,
convertCard,
convertDeckCard,
convertPhase,
convertPositionResponse,
parsePlayerFromMsg,
} from "@/api/ygoAgent/transaction";
import { Context } from "@/container";
import { WebSocketStream } from "@/infra";
import { argmax, computeSetDifference } from "../util";
const { DECK, HAND, MZONE, SZONE, GRAVE, REMOVED, EXTRA } = ygopro.CardZone;
export class YgoAgent {
// TODO
private context?: Context;
private duelId: string = "";
private agentIndex: number = 0;
private prevActionIndex: number = 0;
private mainDeck: number[] = [];
private mainDeckCardMeta: Map<number, CardMeta> = new Map();
private disable: boolean = false;
public attachContext(context: Context) {
this.context = context;
}
public async init() {
if (this.context === undefined)
throw Error("No context beed attached yet!!");
const resp = await createDuel();
if (resp) {
const { duelId, index } = resp;
this.duelId = duelId;
this.agentIndex = index;
this.mainDeck = this.context.sideStore.getSideDeck().main;
this.mainDeckCardMeta = this.mainDeck.reduce((map, item) => {
if (!map.has(item)) {
map.set(item, fetchCard(item));
}
return map;
}, new Map());
} else {
throw Error("Failed to createDuel for agent");
}
}
// When the AI model met some cards which it has not met,
// it can not work any more so we need to disable it.
public setDisable(value: boolean) {
this.disable = value;
}
public getDisable(): boolean {
return this.disable;
}
private genAgentInput(msg: ygopro.StocGameMessage): Input {
if (this.context) {
const mat = this.context.matStore;
const cardStore = this.context.cardStore;
// TODO (ygo-agent): TZONE
const zones = [DECK, HAND, MZONE, SZONE, GRAVE, REMOVED, EXTRA];
const player = parsePlayerFromMsg(msg);
const opponent = 1 - player;
const cards = cardStore.inner
.filter(
(card) =>
zones.includes(card.location.zone) &&
!(
card.location.zone === DECK && card.location.controller === player
),
)
.map((card) => convertCard(card, player));
const cardCodesMe = cardStore.inner
.filter(
(card) =>
zones.includes(card.location.zone) &&
card.location.controller === player,
)
.map((card) => card.code);
const cardCodesMeDeck = computeSetDifference(this.mainDeck, cardCodesMe);
const mainDeckCardMeta = this.mainDeckCardMeta;
// TODO (ygo-agent): 临时方案,有很多边界情况未考虑
const deckCardsMe = cardCodesMeDeck.map((code) =>
convertDeckCard(mainDeckCardMeta.get(code)!),
);
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),
turn: mat.turnCount,
};
const actionMsg = convertActionMsg(msg);
return {
global,
cards: deckCardsMe.concat(cards),
action_msg: actionMsg,
};
} else {
throw Error("No context been attached yet!! ");
}
}
private async sendRequest(req: PredictReq) {
if (this.context) {
const duelId = this.duelId;
const resp = await predictDuel(duelId, req);
if (resp !== undefined) {
this.agentIndex = resp.index;
} else {
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 actionIdx = argmax(preds, (r) => r.prob);
this.prevActionIndex = actionIdx;
const pred = preds[actionIdx];
return pred;
} else {
throw Error("No context been attached yet!! ");
}
}
private preprocess(
conn: WebSocketStream,
msg: ygopro.StocGameMessage,
): boolean {
if (
msg.gameMsg === "select_option" &&
msg.select_option.options.length === 0
) {
sendSelectOptionResponse(conn, 0);
return true;
} else if (msg.gameMsg === "select_chain") {
const selectChain = msg.select_chain;
if (
selectChain.chains.length === 0 ||
(!selectChain.forced && selectChain.special_count === 0)
) {
sendSelectSingleResponse(conn, -1);
return true;
}
} else if (msg.gameMsg === "select_place" && msg.select_place.count !== 1) {
throw Error("Unhandled case during handling MSG_SELECT_PLACE");
} else if (msg.gameMsg === "sort_card") {
// TODO: AI modal currently not support MSG_SORT_CARD, return default order here
sendSortCardResponse(
conn,
msg.sort_card.options.map((option) => option.response),
);
}
return false;
}
// TODO: AI模型需要一直跟踪对局,所以即使玩家关掉AI也需要给模型服务器发请求,
// 只是不会回应ygopro服务器。后面需要将这个函数解耦出来。
public async sendAIPredictAsResponse(
conn: WebSocketStream,
msg: ygopro.StocGameMessage,
enableKuriboh: boolean,
) {
if (this.disable) return;
if (this.context === undefined)
throw Error("No context been attached yet!!");
// preprocess
if (this.preprocess(conn, msg)) return;
const input = this.genAgentInput(msg);
const msgName = input.action_msg.data.msg_type;
const multiSelectMsgs = ["select_card", "select_tribute", "select_sum"];
if (multiSelectMsgs.includes(msgName)) {
switch (msgName) {
case "select_tribute":
case "select_card": {
const msg_ = input.action_msg.data as MultiSelectMsg;
const selected = [];
const responses = [];
while (true) {
msg_.selected = selected;
const req = {
index: this.agentIndex,
input: input,
prev_action_idx: this.prevActionIndex,
};
const response = (await this.sendRequest(req)).response;
if (response !== -1) {
selected.push(this.prevActionIndex);
responses.push(response);
}
if (
(response === -1 || selected.length === msg_.max) &&
enableKuriboh
) {
sendSelectMultiResponse(conn, responses);
break;
}
}
break;
}
case "select_sum":
const msg_ = input.action_msg.data as MsgSelectSum;
const selected = [];
const responses = [];
for (const c of msg_.must_cards) {
responses.push(c.response);
}
while (true) {
msg_.selected = selected;
const req = {
index: this.agentIndex,
input: input,
prev_action_idx: this.prevActionIndex,
};
const pred = await this.sendRequest(req);
const idx = this.prevActionIndex;
selected.push(idx);
responses.push(pred.response);
if (pred.can_finish && enableKuriboh) {
sendSelectMultiResponse(conn, responses);
break;
}
}
break;
}
} else {
const req = {
index: this.agentIndex,
input: input,
prev_action_idx: this.prevActionIndex,
};
const response = (await this.sendRequest(req)).response;
if (enableKuriboh) {
switch (msgName) {
case "announce_attrib":
case "announce_number":
sendSelectOptionResponse(conn, response);
break;
case "select_battlecmd":
sendSelectBattleCmdResponse(conn, response);
break;
case "select_chain":
sendSelectSingleResponse(conn, response);
break;
case "select_yesno":
case "select_effectyn":
sendSelectEffectYnResponse(conn, response === 1);
break;
case "select_idlecmd":
sendSelectIdleCmdResponse(conn, response);
break;
case "select_option":
sendSelectOptionResponse(conn, response);
break;
case "select_position":
sendSelectPositionResponse(conn, convertPositionResponse(response));
break;
case "select_place": {
const place = msg.select_place.places[response];
sendSelectPlaceResponse(conn, {
controller: place.controller,
zone: place.zone,
sequence: place.sequence,
});
break;
}
case "select_unselect_card": {
if (response === -1) {
sendSelectSingleResponse(conn, -1);
} else {
sendSelectMultiResponse(conn, [response]);
}
break;
}
}
}
}
}
}
import { fetchStrings, Region, ygopro } from "@/api";
import { displayOptionModal } from "@/ui/Duel/Message";
import MsgAnnounce = ygopro.StocGameMessage.MsgAnnounce;
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
import { displayAnnounceModal } from "@/ui/Duel/Message/AnnounceModal";
export default async (container: Container, announce: MsgAnnounce) => {
if (matStore.autoSelect) {
// TODO: 如果是开启 AI 模式,不应该调用这个函数
console.log("intercept announce");
await sendAIPredictAsResponse(
container,
announce as unknown as ygopro.StocGameMessage,
);
return;
}
export default async (announce: MsgAnnounce) => {
const type_ = announce.announce_type;
let min = announce.min;
if (
......
......@@ -3,6 +3,7 @@ import { Container } from "@/container";
import { replayStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message";
import { YgoAgent } from "./agent";
import onAnnounce from "./announce";
import onMsgAttack from "./attack";
import onMsgAttackDisable from "./attackDisable";
......@@ -69,19 +70,6 @@ const ActiveList = [
"select_battle_cmd",
"select_unselect_card",
"select_yes_no",
];
const ReplayIgnoreMsg = [
"select_idle_cmd",
"select_place",
"select_card",
"select_chain",
"select_effect_yn",
"select_position",
"select_option",
"select_battle_cmd",
"select_unselect_card",
"select_yes_no",
"select_tribute",
"select_counter",
"select_sum",
......@@ -93,21 +81,41 @@ const ReplayIgnoreMsg = [
export default async function handleGameMsg(
container: Container,
pb: ygopro.YgoStocMsg,
agent?: YgoAgent,
): Promise<void> {
const msg = pb.stoc_game_msg;
if (ActiveList.includes(msg.gameMsg)) {
showWaiting(false);
}
if (replayStore.isReplay && ReplayIgnoreMsg.includes(msg.gameMsg)) return;
if (replayStore.isReplay) return;
console.log(msg.gameMsg);
if (agent && !agent.getDisable()) {
console.info(`Handling msg: ${msg.gameMsg} with YgoAgent`);
const enableKuriboh = container.getEnableKuriboh();
try {
await agent.sendAIPredictAsResponse(container.conn, msg, enableKuriboh);
if (enableKuriboh) return;
} catch (e) {
console.error(`Erros occurs when handling msg ${msg.gameMsg}: ${e}`);
container.setEnableKuriboh(false);
// TODO: I18N
container.context.matStore.error = `AI模型监测到场上存在它没见过的卡片,
因此需要关掉AI辅助功能。\n
请耐心等待开发团队对模型进行优化,感谢!`;
agent.setDisable(true);
}
}
}
switch (msg.gameMsg) {
case "start": {
await onMsgStart(msg.start);
// We should init agent when the MSG_START reached.
if (agent) await agent.init();
break;
}
case "draw": {
......@@ -131,7 +139,7 @@ export default async function handleGameMsg(
break;
}
case "select_idle_cmd": {
onMsgSelectIdleCmd(container, msg.select_idle_cmd);
onMsgSelectIdleCmd(msg.select_idle_cmd);
break;
}
......@@ -155,12 +163,12 @@ export default async function handleGameMsg(
break;
}
case "select_effect_yn": {
await onMsgSelectEffectYn(container, msg.select_effect_yn);
await onMsgSelectEffectYn(msg.select_effect_yn);
break;
}
case "select_position": {
await onMsgSelectPosition(container, msg.select_position);
await onMsgSelectPosition(msg.select_position);
break;
}
......@@ -175,7 +183,7 @@ export default async function handleGameMsg(
break;
}
case "select_battle_cmd": {
onMsgSelectBattleCmd(container, msg.select_battle_cmd);
onMsgSelectBattleCmd(msg.select_battle_cmd);
break;
}
......@@ -185,12 +193,12 @@ export default async function handleGameMsg(
break;
}
case "select_unselect_card": {
await onMsgSelectUnselectCard(container, msg.select_unselect_card);
await onMsgSelectUnselectCard(msg.select_unselect_card);
break;
}
case "select_yes_no": {
await onMsgSelectYesNo(container, msg.select_yes_no);
await onMsgSelectYesNo(msg.select_yes_no);
break;
}
......@@ -220,12 +228,12 @@ export default async function handleGameMsg(
break;
}
case "select_sum": {
onMsgSelectSum(container, msg.select_sum);
onMsgSelectSum(msg.select_sum);
break;
}
case "select_tribute": {
onMsgSelectTribute(container, msg.select_tribute);
onMsgSelectTribute(msg.select_tribute);
break;
}
......@@ -309,7 +317,7 @@ export default async function handleGameMsg(
break;
}
case "announce": {
await onAnnounce(container, msg.announce);
await onAnnounce(msg.announce);
break;
}
......
......@@ -7,30 +7,17 @@ import {
} from "@/stores";
import MsgSelectBattleCmd = ygopro.StocGameMessage.MsgSelectBattleCmd;
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
export default async (
container: Container,
selectBattleCmd: MsgSelectBattleCmd,
) => {
export default async (selectBattleCmd: MsgSelectBattleCmd) => {
const player = selectBattleCmd.player;
const cmds = selectBattleCmd.battle_cmds;
// 先清掉之前的互动性
// TODO: 确认这里在AI托管的模式下是否需要
cardStore.inner.forEach((card) => {
card.idleInteractivities = [];
});
if (matStore.autoSelect) {
console.log("intercept selectBattleCmd");
await sendAIPredictAsResponse(
container,
selectBattleCmd as unknown as ygopro.StocGameMessage,
);
return;
}
cmds.forEach((cmd) => {
const interactType = battleTypeToInteracType(cmd.battle_type);
......
......@@ -2,8 +2,6 @@ import { sendSelectMultiResponse, ygopro } from "@/api";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
......@@ -14,15 +12,6 @@ export default async (container: Container, selectCard: MsgSelectCard) => {
// TODO: handle release_param
if (matStore.autoSelect) {
console.log("intercept selectCard");
await sendAIPredictAsResponse(
container,
selectCard as unknown as ygopro.StocGameMessage,
);
return;
}
if (!cancelable && cards.length === 1) {
// auto send
sendSelectMultiResponse(conn, [cards[0].response]);
......
import { sendSelectSingleResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { ChainSetting, fetchSelectHintMeta, matStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
......@@ -69,16 +68,6 @@ export default async (container: Container, selectChain: MsgSelectChain) => {
}
case 2: // 处理多张
case 3: {
if (matStore.autoSelect) {
// TODO: 确认AI模型是否可以处理其他case的情况
console.log("intercept selectChain");
await sendAIPredictAsResponse(
container,
selectChain as unknown as ygopro.StocGameMessage,
);
return;
}
// 处理强制发动的卡
fetchSelectHintMeta({
selectHintData: 203,
......
import { fetchStrings, Region, type ygopro } from "@/api";
import { CardMeta, fetchCard } from "@/api/cards";
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
import { displayYesNoModal } from "@/ui/Duel/Message";
type MsgSelectEffectYn = ygopro.StocGameMessage.MsgSelectEffectYn;
// 这里改成了 async 不知道有没有影响
export default async (
container: Container,
selectEffectYn: MsgSelectEffectYn,
) => {
if (matStore.autoSelect) {
console.log("intercept selectEffectYn");
await sendAIPredictAsResponse(
container,
selectEffectYn as unknown as ygopro.StocGameMessage,
);
return;
}
export default async (selectEffectYn: MsgSelectEffectYn) => {
const code = selectEffectYn.code;
const location = selectEffectYn.location;
const effect_description = selectEffectYn.effect_description;
......
import { ygopro } from "@/api";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import {
cardStore,
type Interactivity,
......@@ -8,29 +7,17 @@ import {
} from "@/stores";
import MsgSelectIdleCmd = ygopro.StocGameMessage.MsgSelectIdleCmd;
import { Container } from "@/container";
export default async (
container: Container,
selectIdleCmd: MsgSelectIdleCmd,
) => {
export default async (selectIdleCmd: MsgSelectIdleCmd) => {
const player = selectIdleCmd.player;
const cmds = selectIdleCmd.idle_cmds;
// 先清掉之前的互动性
// TODO: 确认这里是否需要在AI托管的时候调用
cardStore.inner.forEach((card) => {
card.idleInteractivities = [];
});
if (matStore.autoSelect) {
console.log("intercept selectIdleCmd");
await sendAIPredictAsResponse(
container,
selectIdleCmd as unknown as ygopro.StocGameMessage,
);
return;
}
cmds.forEach((cmd) => {
const interactType = idleTypeToInteractType(cmd.idle_type);
......
......@@ -6,8 +6,6 @@ import {
type ygopro,
} from "@/api";
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
import { displayOptionModal } from "@/ui/Duel/Message";
export default async (
......@@ -21,15 +19,6 @@ export default async (
return;
}
if (matStore.autoSelect) {
console.log("intercept selectOption");
await sendAIPredictAsResponse(
container,
selectOption as unknown as ygopro.StocGameMessage,
);
return;
}
if (options.length === 1) {
sendSelectOptionResponse(conn, options[0].response);
return;
......
import { sendSelectPlaceResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { InteractType, matStore, placeStore } from "@/stores";
import { InteractType, placeStore } from "@/stores";
type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
......@@ -12,15 +11,6 @@ export default async (container: Container, selectPlace: MsgSelectPlace) => {
return;
}
if (matStore.autoSelect) {
console.log("intercept selectPlace");
await sendAIPredictAsResponse(
container,
selectPlace as unknown as ygopro.StocGameMessage,
);
return;
}
if (selectPlace.places.length === 1) {
const place = selectPlace.places[0];
sendSelectPlaceResponse(conn, {
......
import { ygopro } from "@/api";
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
import { displayPositionModal } from "@/ui/Duel/Message";
type MsgSelectPosition = ygopro.StocGameMessage.MsgSelectPosition;
export default async (
container: Container,
selectPosition: MsgSelectPosition,
) => {
if (matStore.autoSelect) {
console.log("intercept selectPosition");
await sendAIPredictAsResponse(
container,
selectPosition as unknown as ygopro.StocGameMessage,
);
return;
}
export default async (selectPosition: MsgSelectPosition) => {
const _player = selectPosition.player;
const positions = selectPosition.positions.map(
(position) => position.position,
......
......@@ -4,20 +4,7 @@ import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal"
import { fetchCheckCardMeta } from "../utils";
type MsgSelectSum = ygopro.StocGameMessage.MsgSelectSum;
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
export default async (container: Container, selectSum: MsgSelectSum) => {
if (matStore.autoSelect) {
console.log("intercept selectSum");
await sendAIPredictAsResponse(
container,
selectSum as unknown as ygopro.StocGameMessage,
);
return;
}
export default async (selectSum: MsgSelectSum) => {
const {
selecteds: selecteds1,
mustSelects: mustSelect1,
......
......@@ -4,23 +4,7 @@ import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal"
import { fetchCheckCardMeta } from "../utils";
type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
export default async (
container: Container,
selectTribute: MsgSelectTribute,
) => {
if (matStore.autoSelect) {
console.log("intercept selectTribute");
await sendAIPredictAsResponse(
container,
selectTribute as unknown as ygopro.StocGameMessage,
);
return;
}
export default async (selectTribute: MsgSelectTribute) => {
const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
selectTribute.selectable_cards,
);
......
......@@ -6,22 +6,7 @@ import { fetchCheckCardMeta } from "../utils";
import { isAllOnField } from "./util";
type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
export default async (
container: Container,
selectUnselectCards: MsgSelectUnselectCard,
) => {
if (matStore.autoSelect) {
console.log("intercept selectUnselectCards");
await sendAIPredictAsResponse(
container,
selectUnselectCards as unknown as ygopro.StocGameMessage,
);
return;
}
export default async (selectUnselectCards: MsgSelectUnselectCard) => {
const {
finishable,
cancelable,
......
import { getStrings, ygopro } from "@/api";
import { Container } from "@/container";
import { sendAIPredictAsResponse } from "@/service/duel/agent";
import { matStore } from "@/stores";
import { displayYesNoModal } from "@/ui/Duel/Message";
type MsgSelectYesNo = ygopro.StocGameMessage.MsgSelectYesNo;
export default async (container: Container, selectYesNo: MsgSelectYesNo) => {
if (matStore.autoSelect) {
console.log("intercept selectYesNo");
await sendAIPredictAsResponse(
container,
selectYesNo as unknown as ygopro.StocGameMessage,
);
return;
}
export default async (selectYesNo: MsgSelectYesNo) => {
const _player = selectYesNo.player;
const effect_description = selectYesNo.effect_description;
......
import { fetchCard, ygopro } from "@/api";
import { matStore } from "@/stores";
import { displaySortCardModal } from "@/ui/Duel/Message";
type MsgSortCard = ygopro.StocGameMessage.MsgSortCard;
export default async (sortCard: MsgSortCard) => {
if (matStore.autoSelect) {
console.log("intercept selectTribute");
// TODO (ygo-agent): don't sort, should response 255
}
const options = await Promise.all(
sortCard.options.map(async ({ code, response }) => {
const meta = fetchCard(code!);
......
import { flatten } from "lodash-es";
import { v4 as v4uuid } from "uuid";
import { createDuel, fetchCard, ygopro } from "@/api";
import { ygopro } from "@/api";
import { useConfig } from "@/config";
import { sleep } from "@/infra";
import {
......@@ -89,16 +89,6 @@ export default async (start: ygopro.StocGameMessage.MsgStart) => {
// note: 额外卡组的卡会在对局开始后通过`UpdateData` msg更新
const { duelId, index } = (await createDuel())!;
matStore.duelId = duelId;
matStore.agentIndex = index;
matStore.mainDeckCardMeta = matStore.mainDeck.reduce((map, item) => {
if (!map.has(item)) {
map.set(item, fetchCard(item));
}
return map;
}, new Map());
if (replayStore.isReplay) {
replayStart();
}
......
import { Container } from "@/container";
import { WebSocketStream } from "@/infra";
import { YgoAgent } from "./duel/agent";
import handleSocketMessage from "./onSocketMessage";
export async function pollSocketLooper(
container: Container,
conn: WebSocketStream,
) {
await conn.execute((event) => handleSocketMessage(container, event));
export async function pollSocketLooper(container: Container) {
await container.conn.execute((event) =>
handleSocketMessage(container, event),
);
}
export async function pollSocketLooperWithAgent(container: Container) {
const agent = new YgoAgent();
agent.attachContext(container.context);
await container.conn.execute((event) =>
handleSocketMessage(container, event, agent),
);
}
......@@ -7,6 +7,7 @@ import { YgoProPacket } from "@/api/ocgcore/ocgAdapter/packet";
import { Container } from "@/container";
import { replayStore } from "@/stores";
import { YgoAgent } from "./duel/agent";
import handleGameMsg from "./duel/gameMsg";
import handleTimeLimit from "./duel/timeLimit";
import handleDeckCount from "./mora/deckCount";
......@@ -36,12 +37,18 @@ let animation: Promise<void> = Promise.resolve();
export default async function handleSocketMessage(
container: Container,
e: MessageEvent,
agent?: YgoAgent,
) {
// 确保按序执行
animation = animation.then(() => _handle(container, e));
animation = animation.then(() => _handle(container, e, agent));
}
async function _handle(container: Container, e: MessageEvent) {
// FIXME: 下面的所有`handler`中访问`Store`的时候都应该通过`Container`进行访问
async function _handle(
container: Container,
e: MessageEvent,
agent?: YgoAgent,
) {
const packets = YgoProPacket.deserialize(e.data);
for (const packet of packets) {
......@@ -101,7 +108,7 @@ async function _handle(container: Container, e: MessageEvent) {
// 如果不是回放模式,则记录回放数据
replayStore.record(packet);
}
await handleGameMsg(container, pb);
await handleGameMsg(container, pb, agent);
break;
}
......
......@@ -95,12 +95,7 @@ const initialState: Omit<MatState, "reset"> = {
// methods
isMe,
turnCount: 0,
duelId: "",
agentIndex: 0,
prevActionIndex: 0,
mainDeck: [],
mainDeckCardMeta: new Map(),
autoSelect: false,
error: "",
};
export class MatStore implements MatState, NeosStore {
......@@ -118,12 +113,7 @@ export class MatStore implements MatState, NeosStore {
selectUnselectInfo = initialState.selectUnselectInfo;
duelEnd = initialState.duelEnd;
turnCount = initialState.turnCount;
duelId = initialState.duelId;
agentIndex = initialState.agentIndex;
prevActionIndex = initialState.prevActionIndex;
mainDeck = initialState.mainDeck;
mainDeckCardMeta = initialState.mainDeckCardMeta;
autoSelect = initialState.autoSelect;
error = initialState.error;
// methods
isMe = initialState.isMe;
......@@ -154,12 +144,7 @@ export class MatStore implements MatState, NeosStore {
};
this.duelEnd = false;
this.turnCount = 0;
this.duelId = "";
this.agentIndex = 0;
this.prevActionIndex = 0;
this.mainDeck = [];
this.mainDeckCardMeta = new Map();
this.autoSelect = false;
this.error = initialState.error;
}
}
......
import type { CardMeta, ygopro } from "@/api";
import type { ygopro } from "@/api";
// >>> play mat state >>>
......@@ -50,14 +50,8 @@ export interface MatState {
/** 根据自己的先后手判断是否是自己 */
isMe: (player: number) => boolean;
// 下面其中一些貌似可以封装成为`AgentInfo`
turnCount: number;
duelId: string;
agentIndex: number;
prevActionIndex: number;
mainDeck: number[];
mainDeckCardMeta: Map<number, CardMeta>;
autoSelect: boolean;
error: string;
}
export interface InitInfo {
......
......@@ -15,7 +15,7 @@ export enum SideStage {
WAITING = 8, // 观战者等待双方玩家
}
class SideStore implements NeosStore {
export class SideStore implements NeosStore {
stage: SideStage = SideStage.NONE;
// 因为在上一局可能会出现断线重连,
......
......@@ -18,6 +18,7 @@ export const HintNotification = () => {
const toss = snap.tossResult;
const handResults = snap.handResults;
const currentPhase = snap.phase.currentPhase;
const error = snap.error;
const [msgApi, msgContextHolder] = message.useMessage({
maxCount: NeosConfig.ui.hint.maxCount,
......@@ -61,6 +62,12 @@ export const HintNotification = () => {
}
}, [currentPhase]);
useEffect(() => {
if (error !== "") {
msgApi.error(error);
}
}, [error]);
return <>{msgContextHolder}</>;
};
......
......@@ -230,7 +230,10 @@ export const Menu = () => {
const [phaseSwitchItems, setPhaseSwitchItems] = useState<MenuProps["items"]>(
[],
);
const [autoSelect, setAutoSelect] = useState(false);
const [enableKuriboh, setEnableKuriboh] = useState(
container.getEnableKuriboh(),
);
useEffect(() => {
const endResponse = [
......@@ -315,9 +318,9 @@ export const Menu = () => {
const globalDisable = !matStore.isMe(currentPlayer);
const switchAutoSelect = () => {
const newAutoSelect = !autoSelect;
matStore.autoSelect = newAutoSelect;
setAutoSelect(newAutoSelect);
const newValue = !enableKuriboh;
setEnableKuriboh(newValue);
container.setEnableKuriboh(newValue);
};
return (
......@@ -348,7 +351,7 @@ export const Menu = () => {
</DropdownWithTitle>
<Tooltip title="AI">
<Button
icon={autoSelect ? <RobotFilled /> : <RobotOutlined />}
icon={enableKuriboh ? <RobotFilled /> : <RobotOutlined />}
onClick={switchAutoSelect}
type="text"
></Button>
......
......@@ -167,7 +167,8 @@
"UltraPreemptiveServer": "超先行服",
"PlayerNickname": "玩家昵称",
"RoomPasswordOptional": "房间密码(可选)",
"JoinRoom": "加入房间"
"JoinRoom": "加入房间",
"EnableAIAssist": "启用AI辅助功能"
},
"ReplayModal": {
"SelectReplay": "选择回放",
......
......@@ -15,4 +15,16 @@
.select {
margin: 0.25rem 0;
}
.ai-assist-container {
display: flex;
align-items: center;
gap: 1rem;
margin: 0.25rem 0;
span {
font-size: 1rem;
color: #d7e70b;
}
}
}
import { App, Button, Input, Modal } from "antd";
import { App, Button, Input, Modal, Switch } from "antd";
import React, { ChangeEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
......@@ -43,6 +43,7 @@ export const MatchModal: React.FC = ({}) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const navigate = useNavigate();
const { t: i18n } = useTranslation("MatchModal");
const [enableKuriboh, setEnableKuriBoh] = useState(false);
const handlePlayerChange = (event: ChangeEvent<HTMLInputElement>) => {
setPlayer(event.target.value);
......@@ -60,6 +61,7 @@ export const MatchModal: React.FC = ({}) => {
player,
ip: genServerAddress(serverId),
passWd: passwd,
enableKuriboh,
});
};
......@@ -116,6 +118,13 @@ export const MatchModal: React.FC = ({}) => {
]}
onChange={handleServerChange}
/>
<div className={styles["ai-assist-container"]}>
<span>{i18n("EnableAIAssist")}</span>
<Switch
value={enableKuriboh}
onChange={(value) => setEnableKuriBoh(value)}
/>
</div>
<Input
className={styles.input}
type="text"
......
......@@ -5,7 +5,10 @@ import { useConfig } from "@/config";
import { getUIContainer, initUIContainer } from "@/container/compat";
import { initReplaySocket, initSocket } from "@/middleware/socket";
import sqliteMiddleWare, { sqliteCmd } from "@/middleware/sqlite";
import { pollSocketLooper } from "@/service/executor";
import {
pollSocketLooper,
pollSocketLooperWithAgent,
} from "@/service/executor";
const NeosConfig = useConfig();
......@@ -14,6 +17,7 @@ export const connectSrvpro = async (params: {
ip: string;
player: string;
passWd: string;
enableKuriboh?: boolean;
replay?: boolean;
replayData?: ArrayBuffer;
}) => {
......@@ -50,7 +54,7 @@ export const connectSrvpro = async (params: {
initUIContainer(conn);
// execute the event looper
pollSocketLooper(getUIContainer(), conn);
pollSocketLooper(getUIContainer());
} else {
// connect to the ygopro Server
const conn = initSocket(params);
......@@ -59,6 +63,13 @@ export const connectSrvpro = async (params: {
initUIContainer(conn);
// execute the event looper
pollSocketLooper(getUIContainer(), conn);
if (params.enableKuriboh) {
const container = getUIContainer();
container.setEnableKuriboh(true);
pollSocketLooperWithAgent(container);
} else {
pollSocketLooper(getUIContainer());
}
}
};
......@@ -28,7 +28,6 @@ import {
accountStore,
deckStore,
IDeck,
matStore,
Player,
resetUniverse,
RoomStage,
......@@ -67,7 +66,6 @@ export const Component: React.FC = () => {
const updateDeck = (deck: IDeck) => {
sendUpdateDeck(container.conn, deck);
matStore.mainDeck = deck.main;
// 设置side里面的卡组
sideStore.setSideDeck(deck);
};
......
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