From eba115a4fa7233f39704be6d620b565b407e6475 Mon Sep 17 00:00:00 2001 From: notify Date: Sun, 3 Dec 2023 18:45:25 +0800 Subject: [PATCH] =?UTF-8?q?Ai=EF=BC=88=E9=B8=BD=E4=B9=8B=EF=BC=89=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 鸽了一点新AI --- docs/dev/smart-ai.rst | 20 + lua/client/client.lua | 2 +- lua/core/player.lua | 1 + lua/core/util.lua | 29 + lua/server/ai/init.lua | 2 +- lua/server/ai/random_ai.lua | 17 +- lua/server/ai/smart_ai.lua | 1546 +++++---------------------- lua/server/room.lua | 6 +- lua/server/system_enum.lua | 8 + packages/maneuvering/ai/init.lua | 8 +- packages/standard/ai/aux_skills.lua | 85 +- packages/standard/ai/init.lua | 10 +- packages/standard_cards/ai/init.lua | 60 +- 13 files changed, 456 insertions(+), 1338 deletions(-) create mode 100644 docs/dev/smart-ai.rst diff --git a/docs/dev/smart-ai.rst b/docs/dev/smart-ai.rst new file mode 100644 index 00000000..9522df54 --- /dev/null +++ b/docs/dev/smart-ai.rst @@ -0,0 +1,20 @@ +关于类似神杀的Smart-AI的实现思路 +================================== + +AI的目的就是为了响应各种askFor,而Smart-ai则是给了玩家自定义askFor策略的接口。 + +大体框架还是一样的,根据command type去选择执行某个通用函数,再根据各种参数不断 +细化函数执行,最后执行Mod开发者的自定义逻辑。 + +而如何设计这种接口就是要面对的问题了。 + +神杀智慧1:堆积如山的hasSkill +------------------------------ + +神杀一个突出的问题就是各种hasSkill写死,比如判断要不要黑杀某人:直接写死hasSkill +仁王盾啥的 + +神杀智慧2:一次性sort所有卡牌/主动技/视为技 +-------------------------------------------- + +如题,这导致每次都要花秒级甚至分钟级别的时间来出一张牌。 diff --git a/lua/client/client.lua b/lua/client/client.lua index 1fd6aa20..12bfa646 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -739,7 +739,7 @@ fk.client_callback["AskForUseActiveSkill"] = function(jsonData) -- jsonData: [ string skill_name, string prompt, bool cancelable. json extra_data ] local data = json.decode(jsonData) local skill = Fk.skills[data[1]] - local extra_data = json.decode(data[4]) + local extra_data = data[4] for k, v in pairs(extra_data) do skill[k] = v end diff --git a/lua/core/player.lua b/lua/core/player.lua index 77adc759..abc604db 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -382,6 +382,7 @@ end ---@param include_hand bool @ 是否包含真正的手牌 ---@return integer[] function Player:getHandlyIds(include_hand) + include_hand = include_hand or include_hand == nil local ret = include_hand and self:getCardIds("h") or {} for k, v in pairs(self.special_cards) do if k:endsWith("&") then table.insertTable(ret, v) end diff --git a/lua/core/util.lua b/lua/core/util.lua index 83049cbc..d1f7f352 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -85,6 +85,35 @@ function fk.qlist(list) return qlist_iterator, list, -1 end +--- 用于for循环的迭代函数。可以将表按照某种权值的顺序进行遍历,这样不用进行完整排序。 +---@generic T +---@param t T[] +---@param val_func? fun(e: T): integer @ 计算权值的函数,对int[]可不写 +---@param reverse? boolean @ 是否反排?反排的话优先返回权值小的元素 +function fk.sorted_pairs(t, val_func, reverse) + val_func = val_func or function(e) return e end + local t2 = table.simpleClone(t) -- 克隆一次表,用作迭代器上值 + local iter = function() + local max_idx, max, max_val = -1, nil, nil + for i, v in ipairs(t2) do + if not max then + max_idx, max, max_val = i, v, val_func(v) + else + local val = val_func(v) + local checked = val > max_val + if reverse then checked = not checked end + if checked then + max_idx, max, max_val = i, v, val + end + end + end + if max_idx == -1 then return nil, nil end + table.remove(t2, max_idx) + return -1, max, max_val + end + return iter, nil, 1 +end + ---@param func fun(element, index, array) function table:forEach(func) for i, v in ipairs(self) do diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 9b31ca2e..07e56993 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -6,7 +6,7 @@ RandomAI = require "server.ai.random_ai" --[[ 在release版暂时不启动。 SmartAI = require "server.ai.smart_ai" - +---[[ 调试中,暂且不加载额外的AI。 -- load ai module from packages local directories = FileIO.ls("packages") require "packages.standard.ai" diff --git a/lua/server/ai/random_ai.lua b/lua/server/ai/random_ai.lua index a2c39951..1212c958 100644 --- a/lua/server/ai/random_ai.lua +++ b/lua/server/ai/random_ai.lua @@ -6,7 +6,7 @@ local RandomAI = AI:subclass("RandomAI") ---@param self RandomAI ---@param skill ActiveSkill ---@param card Card | nil -local function useActiveSkill(self, skill, card) +function RandomAI:useActiveSkill(skill, card) local room = self.room local player = self.player @@ -62,7 +62,7 @@ end ---@param self RandomAI ---@param skill ViewAsSkill -local function useVSSkill(self, skill, pattern, cancelable, extra_data) +function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data) local player = self.player local room = self.room local precondition @@ -116,11 +116,11 @@ random_cb["AskForUseActiveSkill"] = function(self, jsonData) local skill = Fk.skills[data[1]] local cancelable = data[3] if cancelable and math.random() < 0.25 then return "" end - local extra_data = json.decode(data[4]) + local extra_data = data[4] for k, v in pairs(extra_data) do skill[k] = v end - return useActiveSkill(self, skill) + return RandomAI.useActiveSkill(self, skill) end random_cb["AskForSkillInvoke"] = function(self, jsonData) @@ -202,7 +202,7 @@ random_cb["PlayCard"] = function(self, jsonData) local card = sth local skill = card.skill ---@type ActiveSkill if math.random() > 0.15 then - local ret = useActiveSkill(self, skill, card) + local ret = RandomAI.useActiveSkill(self, skill, card) if ret ~= "" then return ret end table.removeOne(cards, card) else @@ -211,14 +211,14 @@ random_cb["PlayCard"] = function(self, jsonData) elseif sth:isInstanceOf(ActiveSkill) then local active = sth if math.random() > 0.30 then - local ret = useActiveSkill(self, active, nil) + local ret = RandomAI.useActiveSkill(self, active, nil) if ret ~= "" then return ret end end table.removeOne(cards, active) else local vs = sth if math.random() > 0.20 then - local ret = useVSSkill(self, vs) + local ret = self:useVSSkill(vs) -- TODO: handle vs result end table.removeOne(cards, vs) @@ -228,6 +228,9 @@ random_cb["PlayCard"] = function(self, jsonData) return "" end +-- FIXME: for smart ai +RandomAI.cb_table = random_cb + function RandomAI:initialize(player) AI.initialize(self, player) self.cb_table = random_cb diff --git a/lua/server/ai/smart_ai.lua b/lua/server/ai/smart_ai.lua index 1bc971f8..5cb34210 100644 --- a/lua/server/ai/smart_ai.lua +++ b/lua/server/ai/smart_ai.lua @@ -9,1286 +9,322 @@ 但为了实现这个目的就还要去额外实现敌友判断、收益计算等等功能。 为了便于各个拓展快速编写AI,还要封装一些AI判断时常用的函数。 + 本文件包含以下内容: + 1. 基本策略代码:定义各种全局表,以及smart_cb表 + 2. 敌我相关代码:关于如何判断敌我以及更新意向值等 + 3. 十分常用的各种函数(?) + -- TODO: 优化底层逻辑,防止AI每次操作之前都要json.decode一下。 -- TODO: 更加详细的文档 --]] ---@class SmartAI: AI +---@field private _memory table @ AI底层的空间换时间机制 +---@field public friends ServerPlayer[] @ 队友 +---@field public enemies ServerPlayer[] @ 敌人 local SmartAI = AI:subclass("SmartAI") ---[[ - * 数据表准备部分 * - 这部分定义了各种以后决策、拓展等都会用到的表。 - 这些表的内容只要加载完成后就不会改变,所以定义成了全局表的样子。 ---]] - ---- 用来应对Room:askForUseActiveSkill的表。 ----@type table -fk.ai_use_skill = {} - ---- TOdo? Room:askForGeneral暂缺 - ---- 用来应对Room:askForSkillInvoke的表。 ----@type table -fk.ai_skill_invoke = {} - ---- 用来应对Room:askForAG的表。表的键是prompt的第一项。 ----@type table -fk.ai_ask_for_ag = {} - ---- 用来应对出牌阶段空闲时间点如何出牌/使用技能的表。 ----@type table -fk.ai_use_play = {} - ---- 用来应对Room:askForUseCard的表。表的键是prompt的第一项或者牌名,优先prompt。 ----@type table -fk.ai_ask_usecard = {} - ----[effect.card.name] = function(self, effect.card, room:getPlayerById(effect.to), room:getPlayerById(effect.from), positive) ----@type table -fk.ai_nullification = {} - ----[card.name] = {intention = 0, value = 0, priority = 0} ---- ----[skill.name] = {intention = 0, value = 0, priority = 0} ----@type table -fk.ai_card = {} - ----[card.id] = 0 ---- ----[skill.name] = 0 ----@type table -fk.cardValue = {} - ----[prompt:split(":")[1]] = function(self, pattern, prompt, cancelable, extra_data) ---- ----[card_name] = function(self, pattern, prompt, cancelable, extra_data) ----@type table -fk.ai_response_card = {} - ----[reason] = function(self, to, flag) ----@type table -fk.ai_card_chosen = {} - ----[reason] = function(self, to, min, max, flag) -fk.ai_cards_chosen = {} - ----[skill_name] = function(self, choices, prompt, detailed, all_choices) ----@type table -fk.ai_ask_choice = {} - ----[judge.reason] = {judge.pattern,isgood} ----@type table -fk.ai_judge = {} - ----[gameMode] = function(self, to) ---- ----根据游戏模式定义目标敌友值 ---- ----返回的值大于0则为敌,小于0则为友,一般是值大于等于3才会杀目标,小于等于-2才会救目标 ----@type table -fk.ai_objective_level = {} - ---[[ - * SmartAI类成员函数部分 * ---]] - ---[[ - * command处理函数部分 * - 这部分就像RandomAI一样对各种请求类型返回相应的数据。 - 当然了,SmartAI会尽可能做出合乎逻辑的决策。 ---]] - ---@type table local smart_cb = {} ---- 请求发动主动技 ---- ---- 总的请求技,从它分支出各种功能技 -smart_cb["AskForUseActiveSkill"] = function(self, jsonData) - local data = json.decode(jsonData) - local skill = Fk.skills[data[1]] - local prompt = data[2] - local cancelable = data[3] - self:updatePlayers() - local extra_data = json.decode(data[4]) - for k, v in pairs(extra_data) do - skill[k] = v - end - self.use_id = nil - self.use_tos = {} - local ask = fk.ai_use_skill[data[1]] - if type(ask) == "function" then - ask(self, prompt, cancelable, extra_data) - end - if self.use_id then - return json.encode { - card = self.use_id, - targets = self.use_tos - } - end - return "" -end - ---- 请求发动技能 -smart_cb["AskForSkillInvoke"] = function(self, jsonData) - local data = json.decode(jsonData) - local prompt = data[2] - local extra_data = data[3] - local ask = fk.ai_skill_invoke[data[1]] - self:updatePlayers() - if type(ask) == "function" then - return ask(self, extra_data, prompt) and "1" or "" - elseif type(ask) == "boolean" then - return ask and "1" or "" - elseif Fk.skills[data[1]].frequency == 1 then - return "1" - else - return table.random { "1", "" } - end -end - ---- 请求AG -smart_cb["AskForAG"] = function(self, jsonData) - local data = json.decode(jsonData) - local prompt = data[3] - local cancelable = data[2] - local id_list = data[1] - self:updatePlayers() - local ask = fk.ai_ask_for_ag[prompt:split(":")[1]] - if type(ask) == "function" then - ask = ask(self, id_list, cancelable, prompt) - end - if type(ask) ~= "number" then - local cards = table.map(id_list, function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - ask = cards[#cards].id - end - return ask -end - ---- 使用阶段技或卡牌 ----@param self SmartAI @ai系统 ----@param skill ActiveSkill|Card|ViewAsSkill @输入可用的阶段技或卡牌 ----@return string @json使用数据(包含了子卡和目标) -local function usePlaySkill(self, skill) - self.use_id = nil - self.use_tos = {} - Self = self.player - self.special_skill = nil - if skill:isInstanceOf(Card) then - local uc = fk.ai_use_play[skill.name] - if type(uc) == "function" then - uc(self, skill) - end - if self.use_id == nil then - if type(skill.special_skills) == "table" then - for _, sn in ipairs(skill.special_skills) do - uc = fk.ai_use_play[sn] - if type(uc) == "function" then - uc(self, skill) - if self.use_id then - break - end - end - end - end - if skill.type == 3 then - if self.player:getEquipment(skill.sub_type) or #self.player:getCardIds("h") <= self.player.hp then - return "" - end - self.use_id = skill.id - elseif skill.is_damage_card and skill.multiple_targets then - if #self.enemies < #self.friends_noself then - return "" - end - self.use_id = skill.id - end - end - elseif skill:isInstanceOf(ViewAsSkill) then - local selected = {} - local cards = table.map(self.player:getCardIds("&he"), function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, c in ipairs(cards) do - if skill:cardFilter(c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = skill:viewAs(selected) - if tc then - local uc = fk.ai_use_play[tc.name] - if type(uc) == "function" then - uc(self, tc) - if self.use_id then - self.use_id = selected - end - end - end - else - local uc = fk.ai_use_play[skill.name] - if type(uc) == "function" then - uc(self, skill) - end - end - if self.use_id then - if not skill:isInstanceOf(Card) then - self.use_id = json.encode { - skill = skill.name, - subcards = self.use_id - } - end - return json.encode { - card = self.use_id, - targets = self.use_tos, - special_skill = self.special_skill - } - end - return "" -end - ---- 请求使用 ---- ----优先由prompt进行下一级的决策,需要定义self.use_id,如果卡牌需要目标也需要给self.use_tos添加角色id为目标 ---- ----然后若没有定义self.use_id则由card_name再进行决策 -smart_cb["AskForUseCard"] = function(self, jsonData) - local data = json.decode(jsonData) - local pattern = data[2] - local prompt = data[3] - local cancelable = data[4] - local extra_data = data[5] - self:updatePlayers() - self.use_id = nil - self.use_tos = {} - local exp = Exppattern:Parse(data[2] or data[1]) - self.avail_cards = table.filter(self.player:getCardIds("&he"), function(id) - return exp:match(Fk:getCardById(id)) and not self.player:prohibitUse(Fk:getCardById(id)) - end - ) - Self = self.player - local ask = fk.ai_ask_usecard[prompt:split(":")[1]] - if type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - else - local cards = table.map(self.player:getCardIds("&he"), function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, sth in ipairs(self:getActives(pattern)) do - if sth:isInstanceOf(Card) then - if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then - local ret = usePlaySkill(self, sth) - if ret ~= "" then - return ret - end - end - else - local selected = {} - for _, c in ipairs(cards) do - if sth:cardFilter(c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = sth:viewAs(selected) - if tc and tc:matchPattern(pattern) then - local uc = fk.ai_use_play[tc.name] - if type(uc) == "function" then - uc(self, tc) - if self.use_id then - self.use_id = json.encode { - skill = sth.name, - subcards = selected - } - break - end - end - end - end - end - end - ask = fk.ai_ask_usecard[data[1]] - if self.use_id == nil and type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - end - if self.use_id == true then - self.use_id = self.avail_cards[1] - end - if self.use_id then - return json.encode { - card = self.use_id, - targets = self.use_tos - } - end - return "" -end - ----根据事件类型获取所有满足的事件的数据表 ----@param game_event string @事件类型(字符串,省略GameEvent. 例如要获取生效事件数据就输入"CardEffect") ----@param ge any|nil @可输入起始事件 ----@return table @事件数据表 -function SmartAI:eventsData(game_event, ge) - local datas = {} - local _ge = ge or self.room.logic:getCurrentEvent() - while _ge do - if _ge.event == GameEvent[game_event] then - table.insert(datas, _ge.data[1]) - end - _ge = _ge.parent - end - return datas -end - ----请求无懈 ---- ----由锦囊牌名进行下一级决策,给self.use_id定义无懈id进行使用,同时参数包含positive(boolean)来区分正反无懈 ----@param self SmartAI @ai系统 ----@param pattern any ----@param prompt any ----@param cancelable any ----@param extra_data any -fk.ai_ask_usecard.nullification = function(self, pattern, prompt, cancelable, extra_data) - local datas = self:eventsData("CardEffect") - local effect = datas[#datas] --修改了无懈的请求,不用在room.lua里加记录了 - local positive = #datas % 2 == 1 - local ask = fk.ai_nullification[effect.card.name] - if type(ask) == "function" then - ask(self, effect.card, self.room:getPlayerById(effect.to), self.room:getPlayerById(effect.from), positive) - end -end - ----根据事件类型获取满足的事件的数据 ----@param game_event string @事件类型(字符串,省略GameEvent. 例如要获取生效事件数据就输入"CardEffect") ----@return any @事件数据 -function SmartAI:eventData(game_event) - local event = self.room.logic:getCurrentEvent():findParent(GameEvent[game_event], true) - return event and event.data[1] -end - ----请求桃 ----@param self SmartAI @ai系统 ----@param pattern any ----@param prompt any ----@param cancelable any ----@param extra_data any -fk.ai_ask_usecard["#AskForPeaches"] = function(self, pattern, prompt, cancelable, extra_data) - local dying = self:eventData("Dying") - local who = self.room:getPlayerById(dying.who) - if who and self:objectiveLevel(who) < -1 then - local cards = table.map(self.player:getCardIds("&he"), function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, sth in ipairs(self:getActives(pattern)) do - if sth:isInstanceOf(Card) then - self.use_id = sth.id - break - else - local selected = {} - for _, c in ipairs(cards) do - if sth.cardFilter(sth, c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = sth.viewAs(sth, selected) - if tc and tc:matchPattern(pattern) then - self.use_id = json.encode { - skill = sth.name, - subcards = selected - } - break - end - end - end - end -end - -fk.ai_ask_usecard["#AskForPeachesSelf"] = fk.ai_ask_usecard["#AskForPeaches"] - ----修正卡牌价值 ---- ----根据技能会转化的牌名修正卡牌的价值,例如有倾国时黑色手牌的价值会加上闪的价值 ----@param assign string[]|nil @牌名表 -function SmartAI:assignValue(assign) - assign = assign or { "slash", "peach", "jink", "nullification" } - for v, p in ipairs(assign) do - local kept = {} - v = fk.ai_card[p] - v = v and v.value or 3 - for _, sth in ipairs(self:getActives(p)) do - if sth:isInstanceOf(Card) then - fk.cardValue[sth.id] = self:getValue(sth, kept) - else - fk.cardValue[sth.name] = self:getValue(sth, kept) + v - end - table.insert(kept, sth) - end - self.keptCv = nil - end -end - ----获取卡牌价值 ---- ----需要自己定义卡牌或阶段技的价值(直接看标包ai文件) ---- ----当有输入kept时,就是对卡牌价值进行修正,同名卡牌的价值会逐渐递减, ----例如有多张闪时,最高价值的那一张价值保持不变,然后每多一张,多的这张的价值就会减少25%,会不断累积 ----@param card Card @卡牌 ----@param kept string[]|nil @已有同名卡牌表(用来配合修正卡牌价值使用) ----@return number @价值 -function SmartAI:getValue(card, kept) - local v = fk.ai_card[card.name] - v = v and v.value or 0 - if kept then - if card:isInstanceOf(Card) then - if self.keptCv == nil then - self.keptCv = v - end - return v - #kept * 0.25 - else - return (self.keptCv or v) - #kept * 0.25 - end - elseif card:isInstanceOf(Card) then - return fk.cardValue[card.id] or v - else - return fk.cardValue[card.name] or v - end -end - ----获取优先度 ---- ----需要自己定义卡牌或阶段技的优先值(直接看标包ai文件) ----@param card Card @卡牌 ----@return number @优先值 -function SmartAI:getPriority(card) - local v = card and fk.ai_card[card.name] - v = v and v.priority or 0 - if card:isInstanceOf(Card) then - if card:isInstanceOf(Armor) then - v = v + 7 - elseif card:isInstanceOf(Weapon) then - v = v + 3 - elseif card:isInstanceOf(OffensiveRide) then - v = v + 6 - elseif card:isInstanceOf(DefensiveRide) then - v = v + 4 - end - v = v + (13 - card.number) / 100 - v = v + card.suit / 100 - if card:isVirtual() then - v = v - #card.subcards * 0.25 - end - end - return v -end - -fk.compareFunc = { - hp = function(p) - return p.hp - end, - maxHp = function(p) - return p.maxHp - end, - hand = function(p) - return #p:getHandlyIds(true) - end, - equip = function(p) - return #p:getCardIds("e") - end, - maxcards = function(p) - return p.hp - end, - skill = function(p) - return #p:getAllSkills() - end, - defense = function(p) - return p.hp + #p:getHandlyIds(true) - end -} - ----对角色表进行条件排序,由低到高 ----@param players ServerPlayer[] @角色表 ----@param key string|nil @条件(上面compareFunc列举的,默认是状态值) ----@param reverse boolean|nil @反向排序(由高到低) -function SmartAI:sort(players, key, reverse) - key = key or "defense" - local func = fk.compareFunc[key] - if func == nil then - func = fk.compareFunc.defense - end - local function compare_func(a, b) - return func(a) < func(b) - end - table.sort(players, compare_func) - if reverse then - players = table.reverse(players) - end -end - ----排序卡牌表价值,由低到高 ----@param cards Card[] @卡牌表 ----@param reverse boolean|nil @反向排序(由高到低) -function SmartAI:sortValue(cards, reverse) - local function compare_func(a, b) - return self:getValue(a) < self:getValue(b) - end - table.sort(cards, compare_func) - if reverse then - cards = table.reverse(cards) - end -end - ----排序阶段技和卡牌表优先值,由高到低 ----@param cards table @阶段技和卡牌表 ----@param reverse boolean|nil @反向排序(由低到高) -function SmartAI:sortPriority(cards, reverse) - local function compare_func(a, b) - local va = a and self:getPriority(a) or 0 - local vb = b and self:getPriority(b) or 0 - if va == vb then - va = a and self:getValue(a) or 0 - vb = b and self:getValue(b) or 0 - end - return va > vb - end - table.sort(cards, compare_func) - if reverse then - cards = table.reverse(cards) - end -end - ----请求打出 ---- ----优先按照prompt提示信息进行下一级决策,需要定义self.use_id,然后可以根据card_name再进行决策 -smart_cb["AskForResponseCard"] = function(self, jsonData) - local data = json.decode(jsonData) - local pattern = data[2] - local prompt = data[3] - local cancelable = data[4] - local extra_data = data[5] - self:updatePlayers() - self.use_id = nil - local ask = fk.ai_response_card[prompt:split(":")[1]] - if type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - else - ask = fk.ai_response_card[data[1]] - if type(ask) == "function" then - ask(self, pattern, prompt, cancelable, extra_data) - else - local effect = self:eventData("CardEffect") - if effect and (effect.card.multiple_targets or self:isEnemie(effect.from, effect.to)) then - self:setUseId(pattern) - end - end - end - if self.use_id then - return json.encode { - card = self.use_id, - targets = {} - } - end - return "" -end - ----根据pattern获取可用的卡牌或转化技 ---- ----默认是按照优先度从高到低排序 ----@param pattern string @可用条件 ----@return table @可用卡牌和转化技表 -function SmartAI:getActives(pattern) - local cards = table.map(self.player:getCardIds("&he"), function(id) - return Fk:getCardById(id) - end - ) - local exp = Exppattern:Parse(pattern) - cards = table.filter(cards, function(c) - return exp:match(c) - end - ) - table.insertTable(cards, - table.filter(self.player:getAllSkills(), function(s) - return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) - end - ) - ) - self:sortPriority(cards) - return cards -end - ----根据pattern直接定义self.use_id ---- ----默认是按照优先度从高到低排序且优先使用价值低的卡牌或转化技 ----@param pattern string @可用条件 -function SmartAI:setUseId(pattern) - local cards = table.map(self.player:getCardIds("&he"), function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, sth in ipairs(self:getActives(pattern)) do - if sth:isInstanceOf(Card) then - self.use_id = sth.id - break - else - local selected = {} - for _, c in ipairs(cards) do - if sth:cardFilter(c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = sth:viewAs(selected) - if tc and tc:matchPattern(pattern) then - self.use_id = json.encode { - skill = sth.name, - subcards = selected - } - break - end - end - end -end - ----根据pattern获取可用的转化技 ----@param pattern string @可用条件 ----@return table @可用转化技表 -function SmartAI:cardsView(pattern) - local actives = table.filter(self.player:getAllSkills(), function(s) - return s:isInstanceOf(ViewAsSkill) and s:enabledAtResponse(self.player, pattern) - end - ) - return actives -end - ----空闲点使用 -smart_cb["PlayCard"] = function(self, jsonData) - local cards = table.map(self.player:getHandlyIds(true), function(id) - return Fk:getCardById(id) - end - ) - cards = table.filter(cards, function(c) - return c.skill:canUse(self.player, c) and not self.player:prohibitUse(c) - end) - table.insertTable(cards, - table.filter(self.player:getAllSkills(), function(s) - return s:isInstanceOf(ActiveSkill) and s:canUse(self.player) - or s:isInstanceOf(ViewAsSkill) and s:enabledAtPlay(self.player) - end - ) - ) - if #cards < 1 then return "" end - self:updatePlayers() - self:sortPriority(cards) - for _, sth in ipairs(cards) do - local ret = usePlaySkill(self, sth) - if ret ~= "" then - return ret - end - end - return "" -end - ----请求选择角色区域牌 ---- ----按照reason原因进行下一级决策,需返回选择的牌id,同时设置有兜底决策 -smart_cb["AskForCardChosen"] = function(self, jsonData) - local data = json.decode(jsonData) - local to = self.room:getPlayerById(data[1]) - local chosen = fk.ai_card_chosen[data[3]] - if type(chosen) == "function" then - return chosen(self, to, data[2]) or "" - elseif table.contains(self.friends, to) then - if string.find(data[2], "j") then - local jc = to:getCardIds("j") - if #jc > 0 then - return table.random(jc) - end - end - else - if string.find(data[2], "h") then - local hc = to:getCardIds("h") - if #hc == 1 then - return hc[1] - end - end - if string.find(data[2], "e") then - local ec = to:getCardIds("e") - if #ec > 0 then - return table.random(ec) - end - end - if string.find(data[2], "h") then - local hc = to:getCardIds("h") - if #hc > 0 then - return table.random(hc) - end - end - end - return "" -end - ----请求选择角色区域多张牌 ---- ----按照reason原因进行下一级决策,需返回选择的牌id表,同时设置有兜底决策 -smart_cb["AskForCardsChosen"] = function(self, jsonData) - local data = json.decode(jsonData) - local to = self.room:getPlayerById(data[1]) - local min = data[2] - local max = data[3] - local flag = data[4] - local ids = {} - local chosen = fk.ai_cards_chosen[data[5]] - if type(chosen) == "function" then - return chosen(self, to, min, max, flag) - elseif table.contains(self.friends, to) then - if string.find(flag, "j") then - for _, id in ipairs(to:getCardIds("j")) do - if #ids= #hc then - for _, id in ipairs(hc) do - table.insert(ids,id) - end - end - end - if string.find(flag, "e") then - for _, id in ipairs(to:getCardIds("e")) do - if #ids= min and json.encode(ids) or "" -end - ----请求选择选项 ---- ----按照skill_name进行下一级决策,需返回要选择的选项,兜底决策是随机选择 -smart_cb["AskForChoice"] = function(self, jsonData) - local data = json.decode(jsonData) - local choices = data[1] ---@type string[] - local all_choices = data[2] - local prompt = data[4] - local detailed = data[5] - local chosen = fk.ai_ask_choice[data[3]] - if type(chosen) == "function" then - chosen = chosen(self, choices, prompt, detailed, all_choices) - end - return table.contains(choices,chosen) and chosen or table.random(choices) -end - -fk.ai_judge.indulgence = { ".|.|heart", true } -fk.ai_judge.lightning = { ".|2~9|spade", false } -fk.ai_judge.supply_shortage = { ".|.|club", true } - ----改判,输出要改判牌的id ---- ----需要自己定义判定的好坏在fk.ai_judge,例如闪电fk.ai_judge.lightning = { ".|2~9|spade", false },第一个值是满足的条件,第二个值是满足条件后的好坏 ----@param cards Card[] @可用改判的卡牌表 ----@param exchange boolean|nil @是否可交换(类似鬼道) ----@return number|nil @改判牌id -function SmartAI:getRetrialCardId(cards, exchange) - local judge = self:eventData("Judge") - local ai_judge = fk.ai_judge[judge.reason] or {judge.pattern,true} - local isgood = judge.card:matchPattern(ai_judge[1])==ai_judge[2] - local canRetrial = {} - self:sortValue(cards) - if exchange then - for _, c in ipairs(cards) do - if c:matchPattern(judge.pattern) == isgood then - table.insert(canRetrial, c) - end - end - else - if isgood then - if self:isFriend(judge.who) then - return - end - elseif self:isEnemie(judge.who) then - return - end - end - for _, c in ipairs(cards) do - if self:isFriend(judge.who) and c:matchPattern(ai_judge[1])==ai_judge[2] - or self:isEnemie(judge.who) and c:matchPattern(ai_judge[1])~=ai_judge[2] - then - table.insert(canRetrial, c) - end - end - if #canRetrial > 0 then - return canRetrial[1].id - end -end - ----请求观星 ----@param self SmartAI @ai系统 ----@param jsonData any @总数据 ----@return string @json放置顶和底的牌id表 -smart_cb["AskForGuanxing"] = function(self, jsonData) - local data = json.decode(jsonData) - local cards = table.map(data.cards, function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - local function table_clone(self) - local t = {} - for _, r in ipairs(self) do - table.insert(t, r) - end - return t - end - local top = {} - if self.room.current.phase < Player.Play then - local jt = table.map(self.room.current:getCardIds("j"), function(id) - return Fk:getCardById(id) - end - ) - if #jt > 0 then - for _, j in ipairs(table.reverse(jt)) do - local tj = fk.ai_judge[j.name] - if tj then - for _, c in ipairs(table_clone(cards)) do - if tj[2] == c:matchPattern(tj[1]) and #top < data.max_top_cards then - table.insert(top, c.id) - table.removeOne(cards, c) - tj = 1 - break - end - end - end - if tj ~= 1 and #cards > 0 and #top < data.max_top_cards then - table.insert(top, cards[1].id) - table.remove(cards, 1) - end - end - end - self:sortValue(cards, true) - for _, c in ipairs(table_clone(cards)) do - if #top < data.max_top_cards and c.skill:canUse(self.player, c) and usePlaySkill(self, c) ~= "" then - table.insert(top, c.id) - table.removeOne(cards, c) - break - end - end - end - for _, c in ipairs(table_clone(cards)) do - if #top < data.min_top_cards then - table.insert(top, c.id) - table.removeOne(cards, c) - break - end - end - return json.encode { - top, - table.map(cards, function(c) - return c.id - end - ) - } -end - ----更新存活身份数 ----@param room Room ----@return table -local function aliveRoles(room) - fk.alive_roles = { - lord = 0, - loyalist = 0, - rebel = 0, - renegade = 0 - } - for _, ap in ipairs(room.players) do - fk.alive_roles[ap.role] = 0 - end - for _, ap in ipairs(room.alive_players) do - fk.alive_roles[ap.role] = fk.alive_roles[ap.role] + 1 - end - return fk.alive_roles -end - -fk.ai_objective_level["aaa_role_mode"] = function(self, to)--身份局的目标敌友值定义 - local ars = aliveRoles(self.room) - if self.role == "renegade" then - if to.role == "lord" then - return -1 - elseif ars.rebel < 1 then - return 4 - elseif self.ai_role[to.id] == "loyalist" then - return ars.lord + ars.loyalist - ars.rebel - elseif self.ai_role[to.id] == "rebel" then - local r = ars.rebel - ars.lord + ars.loyalist - if r >= 0 then - return 3 - else - return r - end - end - elseif self.role == "lord" or self.role == "loyalist" then - if self.ai_role[to.id] == "rebel" then - return 5 - elseif to.role == "lord" then - return -4 - elseif ars.rebel < 1 then - if self.role == "lord" then - return self.explicit_renegade and self.ai_role[to.id] == "renegade" and 4 or to.hp > 1 and 2 or 0 - elseif self.explicit_renegade then - return self.ai_role[to.id] == "renegade" and 4 or -1 - else - return 3 - end - elseif self.ai_role[to.id] == "loyalist" then - return -2 - elseif self.ai_role[to.id] == "renegade" then - local r = ars.lord + ars.loyalist - ars.rebel - if r <= 0 then - return r - else - return 3 - end - end - elseif self.role == "rebel" then - if to.role == "lord" then - return 5 - elseif self.ai_role[to.id] == "loyalist" then - return 4 - elseif self.ai_role[to.id] == "rebel" then - return -2 - elseif self.ai_role[to.id] == "renegade" then - local r = ars.rebel - ars.lord + ars.loyalist - if r > 0 then - return 1 - else - return r - end - end - end -end - ----判定目标敌友值 ---- ----大于0为敌方,小于0为友方 ----@param to ServerPlayer @判断目标 ----@return number -function SmartAI:objectiveLevel(to) - if type(to) == "number" then - to = self.room:getPlayerById(to) - end - if self.player.id == to.id then - return -3 - elseif #self.room.alive_players < 3 then - return 5 - end - local level = fk.ai_objective_level[self.room.settings.gameMode] - if type(level) == "function" then - level = level(self, to) - else - if self.role == "renegade" or to.role == "renegade" then - self.explicit_renegade = true - end - if to.role == self.role - or to.role == "lord" and self.role == "loyalist" - or to.role == "loyalist" and self.role == "lord" then - level = -2 - else - level = 3 - end - end - return level or 0 -end - ----更新场上敌友 -function SmartAI:updatePlayers() - self.role = self.player.role - self.enemies = {} - self.friends = {} - self.friends_noself = {} - - local function compare_func(a, b) - local v1 = self.role_value[a.id].rebel - local v2 = self.role_value[b.id].rebel - if v1 == v2 then - v1 = self.role_value[a.id].renegade - v2 = self.role_value[b.id].renegade - end - return v1 > v2 - end - local aps = self.room.alive_players - table.sort(aps, compare_func) - self.explicit_renegade = false - local ars = aliveRoles(self.room) - local rebel, renegade, loyalist = 0, 0, 0 - for _, ap in ipairs(aps) do - if ap.role == "lord" then - self.ai_role[ap.id] = "loyalist" - elseif self.role_value[ap.id].rebel > 50 and ars.rebel > rebel then - rebel = rebel + 1 - self.ai_role[ap.id] = "rebel" - elseif self.role_value[ap.id].renegade > 50 and ars.renegade > renegade then - renegade = renegade + 1 - self.ai_role[ap.id] = "renegade" - self.explicit_renegade = self.role_value[ap.id].renegade > 100 - elseif self.role_value[ap.id].rebel < -50 and ars.loyalist > loyalist then - loyalist = loyalist + 1 - self.ai_role[ap.id] = "loyalist" - else - self.ai_role[ap.id] = "neutral" - end - end - self.room:setTag("ai_role", self.ai_role) - local neutrality = {} - for n, p in ipairs(aps) do - n = self:objectiveLevel(p) - if n < 0 then - table.insert(self.friends, p) - if p.id ~= self.player.id then - table.insert(self.friends_noself, p) - end - elseif n > 0 then - table.insert(self.enemies, p) - else - table.insert(neutrality, p) - end - end - self:assignValue() - --[[ - if self.enemies<1 and #neutrality>0 and#self.toUse<3 and self:getOverflow()>0 then - function compare_func(a,b) - return sgs.getDefense(a) 0 or role_value[player.id].rebel > 0 and intention < 0 then - role_value[player.id].renegade = role_value[player.id].renegade + intention * (100 - role_value[player.id].renegade) / 200 - end - local function compare_func(a, b) - local v1 = role_value[a.id].rebel - local v2 = role_value[b.id].rebel - if v1 == v2 then - v1 = role_value[a.id].renegade - v2 = role_value[b.id].renegade - end - return v1 > v2 - end - local aps = player.room.alive_players - table.sort(aps, compare_func) - player.explicit_renegade = false - local ars = aliveRoles(player.room) - local rebel, renegade, loyalist = 0, 0, 0 - for _, ap in ipairs(aps) do - if ap.role == "lord" then - ai_role[ap.id] = "loyalist" - elseif role_value[ap.id].rebel > 50 and ars.rebel > rebel then - rebel = rebel + 1 - ai_role[ap.id] = "rebel" - elseif role_value[ap.id].renegade > 50 and ars.renegade > renegade then - renegade = renegade + 1 - ai_role[ap.id] = "renegade" - player.explicit_renegade = role_value[ap.id].renegade > 100 - elseif role_value[ap.id].rebel < -50 and ars.loyalist > loyalist then - loyalist = loyalist + 1 - ai_role[ap.id] = "loyalist" - else - ai_role[ap.id] = "neutral" - end - end --[[ - fk.qWarning(--提示身份值变化信息,消除注释后就会在调试框中显示 - player.general .. - " " .. - intention .. - " " .. - ai_role[player.id] .. - " rebelValue:" .. role_value[player.id].rebel .. " renegadeValue:" .. role_value[player.id].renegade - ) --]] + return nil end - player.room:setTag("ai_role", ai_role) - player.room:setTag("role_value", role_value) end ---[[ -function SmartAI:filterEvent(event, player, data) -end--]] ---增加全局触发技,这样就不用在gamelogic.lua里增加接口了 -local filterEvent = fk.CreateTriggerSkill { - name = "filter_event", - events = { - fk.TargetSpecified, - --fk.GameStart, - --fk.AfterCardsMove, - fk.CardUsing - }, - priority = -1, - global = true, - can_trigger = function(self, event, target, player, data) - return target == nil or target == player +-- 面板相关交互:对应操控手牌区、技能面板、直接选择目标的交互 +-- 对应UI中的"responding"状态和"playing"状态 +-- AI代码需要像实际操作UI那样完成以下几个任务: +-- * 点击技能按钮(出牌阶段或者打算使用ViewAsSkill) +-- * 技能如果带有interaction,则选择interaction +-- * 如果需要的话点选手牌 +-- * 选择目标 +-- * 点确定 +-- 这些步骤归结起来,就是让AI想办法返回如下定义的UseReply +-- 或者返回nil表示点取消 +--=================================================== + +---@class UseReply +---@field card integer|string|nil @ string情况下是json.encode后 +---@field targets integer[]|nil +---@field special_skill string @ 出牌阶段空闲点使用实体卡特有 +---@field interaction_data any @ 因技能而异,一般都是nil + +---@param card integer|table|nil +---@param targets integer[]|nil +---@param special_skill string|nil +---@param interaction_data any +function SmartAI:buildUseReply(card, targets, special_skill, interaction_data) + if type(card) == "table" then card = json.encode(card) end + return { + card = card, + targets = targets or {}, + special_skill = special_skill, + interaction_data = interaction_data, + } +end + +-- AskForUseActiveSkill: 询问发动主动技/视为技 +-- * 此处 UseReply.card 必定由 json.encode 而来 +-- * 且原型为 { skill = skillName, subcards = integer[] } +---------------------------------------------------------- + +---@type table +fk.ai_active_skill = {} + +smart_cb["AskForUseActiveSkill"] = function(self, jsonData) + local data = json.decode(jsonData) + local skillName, prompt, cancelable, extra_data = table.unpack(data) + + local skill = Fk.skills[skillName] + for k, v in pairs(extra_data) do + skill[k] = v + end + + local ret = self:callFromTable(fk.ai_active_skill, nil, skillName, + self, prompt, cancelable, extra_data) + + if ret then return json.encode(ret) end + if cancelable then return "" end + return RandomAI.cb_table["AskForUseActiveSkill"](self, jsonData) +end + +-- AskForUseCard: 询问使用卡牌 +-- 判断函数一样返回UseReply,此时卡牌可能是integer或者string +-- 为string的话肯定是由ViewAsSkill转化而来 +-- 真的要考虑ViewAsSkill吗,害怕 +--------------------------------------------------------- + +--- 键是prompt的第一项或者牌名,优先prompt,其次name,实在不行trueName。 +---@type table +fk.ai_use_card = {} + +local defauld_use_card = function(self, pattern, _, cancelable, exdata) + if cancelable then return nil end + local cards = self:getCards(pattern, "use", exdata) + if #cards == 0 then return nil end + + -- TODO: 目标 + return self:buildUseReply(cards[1].id) +end + +--- 请求使用,先试图使用prompt,再试图使用card_name,最后交给随机AI +smart_cb["AskForUseCard"] = function(self, jsonData) + local data = json.decode(jsonData) + local card_name, pattern, prompt, cancelable, extra_data = table.unpack(data) + + local prompt_prefix = prompt:split(":")[1] + local key + if fk.ai_use_card[prompt_prefix] then + key = prompt_prefix + elseif fk.ai_use_card[card_name] then + key = card_name + else + local tmp = card_name:split("__") + key = tmp[#tmp] + end + local ret = self:callFromTable(fk.ai_use_card, defauld_use_card, key, + self, pattern, prompt, cancelable, extra_data) + + if ret then return json.encode(ret) end + if cancelable then return "" end + return RandomAI.cb_table["AskForUseCard"](self, jsonData) +end + +-- AskForResponseCard: 询问打出卡牌 +-- 注意事项同前 +------------------------------------- + +-- 一样的牌名或者prompt做键优先prompt +---@type table +fk.ai_response_card = {} + +local defauld_response_card = function(self, pattern, _, cancelable) + if cancelable then return nil end + local cards = self:getCards(pattern, "response") + if #cards == 0 then return nil end + return self:buildUseReply(cards[1].id) +end + +-- 同前 +smart_cb["AskForResponseCard"] = function(self, jsonData) + local data = json.decode(jsonData) + local card_name, pattern, prompt, cancelable, extra_data = table.unpack(data) + + local prompt_prefix = prompt:split(":")[1] + local key + if fk.ai_response_card[prompt_prefix] then + key = prompt_prefix + elseif fk.ai_response_card[card_name] then + key = card_name + else + local tmp = card_name:split("__") + key = tmp[#tmp] + end + local ret = self:callFromTable(fk.ai_response_card, defauld_response_card, key, + self, pattern, prompt, cancelable, extra_data) + + if ret then return json.encode(ret) end + if cancelable then return "" end + return RandomAI.cb_table["AskForResponseCard"](self, jsonData) +end + +-- PlayCard: 出牌阶段空闲时间点使用牌/技能 +-- 老规矩得丢一个UseReply回来,但是自由度就高得多了 +-- 需要完成的任务:从众多亮着的卡、技能中选一个 +-- 考虑要不要用?用的话就用,否则选下个 +-- 至于如何使用,可以复用askFor中的函数 +----------------------------------------------- +smart_cb["PlayCard"] = function(self) + local extra_use_data = { playing = true } + local cards = self:getCards(".", "use", extra_use_data) + + local card_names = {} + for _, cd in ipairs(cards) do + -- TODO: 视为技 + table.insertIfNeed(card_names, cd.name) + end + -- TODO: 主动技 + + -- 第二步:考虑使用其中之一 + local value_func = function(str) return #str end + for _, name in fk.sorted_pairs(card_names, value_func) do + if true then + local ret = self:callFromTable(fk.ai_use_card, nil, + fk.ai_use_card[name] and name or name:split("__")[2], + self, name, "", true, extra_use_data) + + if ret then return json.encode(ret) end + end + end + + return "" +end + +--------------------------------------------------------------------- + +-- 其他交互:不涉及面板而是基于弹窗式的交互 +-- 这块就灵活变通了,没啥非常通用的回复格式 +-- ======================================== + +-- AskForSkillInvoke +-- 只能选择确定或者取消的交互。 +-- 函数返回true或者false即可。 +----------------------------- + +---@type table +fk.ai_skill_invoke = {} + +smart_cb["AskForSkillInvoke"] = function(self, jsonData) + local data = json.decode(jsonData) + local skillName, prompt = data[1], data[2] + local ask = fk.ai_skill_invoke[skillName] + + if type(ask) == "function" then + return ask(self, prompt) and "1" or "" + elseif type(ask) == "boolean" then + return ask and "1" or "" + elseif Fk.skills[skillName].frequency == Skill.Frequent then + return "1" + else + return RandomAI.cb_table["AskForSkillInvoke"](self, jsonData) + end +end + +-- 敌友判断相关。 +-- 目前才开始,做个明身份打牌的就行了。 +--======================================== + +---@param target ServerPlayer +function SmartAI:isFriend(target) + if Self.role == target.role then return true end + local t = { "lord", "loyalist" } + if table.contains(t, Self.role) and table.contains(t, target.role) then return true end + if Self.role == "renegade" or target.role == "renegade" then return math.random() < 0.5 end + return false +end + +---@param target ServerPlayer +function SmartAI:isEnemy(target) + return not self:isFriend(target) +end + +-- 排序相关函数。 +-- 众所周知AI要排序,再选出尽可能最佳的选项。 +-- 这里提供了常见的完整排序和效率更高的不完整排序。 +--================================================= + +-- sorted_pairs 见 core/util.lua + +-- 合法性检测相关 +-- 以后估计会单开个合法性类然后改成套壳吧 +--================================================= + +-- TODO: 这东西估计会变成一个单独模块 +local invalid_func_table = { + use = function(player, card, extra_data) + local playing = extra_data and extra_data.playing + return Player.prohibitUse(player, card) or (playing and not player:canUse(card)) end, - on_trigger = function(self, event, target, player, data) - local room = player.room - if event == fk.TargetSpecified then - local callback = fk.ai_card[data.card.name] - callback = callback and callback.intention - if type(callback) == "function" then - for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do - p = room:getPlayerById(p) - local intention = callback(p.ai, data.card, room:getPlayerById(data.from)) - if type(intention) == "number" then - updateIntention(room:getPlayerById(data.from), p, intention) - end - end - elseif type(callback) == "number" then - for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do - p = room:getPlayerById(p) - updateIntention(room:getPlayerById(data.from), p, callback) - end - end - elseif event == fk.CardUsing then - if data.card.name == "nullification" then - local datas = player.ai:eventsData("CardEffect") - local effect = datas[#datas] - local to = room:getPlayerById(effect.to) - local from = room:getPlayerById(data.from) - local callback = fk.ai_card[effect.card.name] - callback = callback and callback.intention - if #datas % 2 == 1 then - if type(callback) == "function" then - callback = callback(to.ai, effect.card, from) - if type(callback) == "number" then - updateIntention(from, to, -callback) - end - elseif type(callback) == "number" then - updateIntention(from, to, -callback) - end - else - if type(callback) == "function" then - callback = callback(to.ai, effect.card, from) - if type(callback) == "number" then - updateIntention(from, to, callback) - end - elseif type(callback) == "number" then - updateIntention(from, to, callback) - end - end - end - elseif event == fk.AfterCardsMove then - end - end + response = Player.prohibitResponse, + discard = Player.prohibitDiscard, } -Fk:addSkill(filterEvent) ----判断目标是否虚弱 ----@param player ServerPlayer @目标,默认是自己 ----@param getAP boolean|nil @默认包含目标可知的桃酒(未完成) ----@return boolean -function SmartAI:isWeak(player, getAP) - player = player or self.player - if type(player) == "number" then - player = self.room:getPlayerById(player) - end - return player.hp < 2 or player.hp <= 2 and #player:getCardIds("&h") <= 2 -end +--- 根据pattern获得所有手中的牌。 +---@param pattern string +---@param validator? string @ 合法检测,须为use, response, discard之一或空 +---@param extra_data? UseExtraData @ 出牌阶段用 +---@return Card[] +function SmartAI:getCards(pattern, validator, extra_data) + validator = validator or "" + extra_data = extra_data or Util.DummyTable + local invalid_func = invalid_func_table[validator] or Util.FalseFunc + local exp = Exppattern:Parse(pattern) ----判断目标是否是友军 ---- ----如果有tp就判断p和tp之间是否是友军,否则就判断p是不是自己的友军 ----@param p ServerPlayer @目标 ----@param tp ServerPlayer|nil @比较目标 ----@return boolean|nil @有时候需要nil来判断中立角色 -function SmartAI:isFriend(p, tp) - if tp then - local bt = self:isFriend(p) - return bt ~= nil and bt == self:isFriend(tp) - end - local ve = self:objectiveLevel(p) - if ve < 0 then - return true - elseif ve > 0 then - return false - end -end + local cards = table.map(self.player:getHandlyIds(), Util.Id2CardMapper) + local ret = table.filter(cards, function(c) + return exp:match(c) and not invalid_func(self.player, c, extra_data) + end) ----判断目标是否是敌军 ---- ----如果有tp就判断p和tp之间是否是敌军,否则就判断p是不是自己的敌军 ----@param p ServerPlayer @目标 ----@param tp ServerPlayer|nil @比较目标 ----@return boolean|nil @有时候需要nil来判断中立角色 -function SmartAI:isEnemie(p, tp) - if tp then - local bt = self:isFriend(p) - return bt ~= nil and bt ~= self:isFriend(tp) - end - local ve = self:objectiveLevel(p) - if ve > 0 then - return true - elseif ve < 0 then - return false - end + -- TODO: 考虑视为技,这里可以再返回一些虚拟牌 + return ret end return SmartAI diff --git a/lua/server/room.lua b/lua/server/room.lua index 810ad5c8..b4a3676a 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -1067,7 +1067,7 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra local command = "AskForUseActiveSkill" self:notifyMoveFocus(player, extra_data.skillName or skill_name) -- for display skill name instead of command name - local data = {skill_name, prompt, cancelable, json.encode(extra_data)} + local data = {skill_name, prompt, cancelable, extra_data} Fk.currentResponseReason = extra_data.skillName local result = self:doRequest(player, command, json.encode(data)) @@ -1951,7 +1951,7 @@ end ---@param pattern string|nil @ 使用牌的规则,默认就是card_name的值 ---@param prompt string|nil @ 提示信息 ---@param cancelable bool @ 能否点取消 ----@param extra_data integer|nil @ 额外信息 +---@param extra_data? UseExtraData @ 额外信息 ---@param event_data CardEffectEvent|nil @ 事件信息 ---@return CardUseStruct | nil @ 返回关于本次使用牌的数据,以便后续处理 function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extra_data, event_data) @@ -2683,7 +2683,7 @@ function Room:handleCardEffect(event, cardEffectEvent) local players = {} Fk.currentResponsePattern = "nullification" for _, p in ipairs(self.alive_players) do - local cards = p:getHandlyIds(true) + local cards = p:getHandlyIds() for _, cid in ipairs(cards) do if Fk:getCardById(cid).trueName == "nullification" and diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index f649d1b6..4a7d889b 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -84,6 +84,14 @@ fk.IceDamage = 4 ---@field public who integer ---@field public damage DamageStruct +--- askForUseCard中的extra_data +---@class UseExtraData +---@field public must_targets? integer[] @ 必须选的目标(?) +---@field public exclusive_targets? integer[] @ ?? +---@field public bypass_distances? boolean @ 无距离限制? +---@field public bypass_times? boolean @ 无次数限制? +---@field public playing? boolean @ (AI专用) 出牌阶段? + ---@class CardUseStruct ---@field public from integer ---@field public tos TargetGroup diff --git a/packages/maneuvering/ai/init.lua b/packages/maneuvering/ai/init.lua index 0ad51f2d..7785abb0 100644 --- a/packages/maneuvering/ai/init.lua +++ b/packages/maneuvering/ai/init.lua @@ -1,3 +1,4 @@ +--[[ fk.ai_card.thunder__slash = fk.ai_card.slash fk.ai_use_play.thunder__slash = fk.ai_use_play.slash fk.ai_card.fire__slash = fk.ai_card.slash @@ -98,7 +99,7 @@ end fk.ai_discard["fire_attack_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) local use = self:eventData("UseCard") for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do - if self:isEnemie(p) then + if self:isEnemy(p) then local cards = table.map(self.player:getCardIds("h"), function(id) return Fk:getCardById(id) end) @@ -122,7 +123,7 @@ fk.ai_nullification.fire_attack = function(self, card, to, from, positive) end end else - if self:isEnemie(to) and #to:getCardIds("h") > 0 and #from:getCardIds("h") > 1 then + if self:isEnemy(to) and #to:getCardIds("h") > 0 and #from:getCardIds("h") > 1 then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -154,7 +155,7 @@ fk.ai_nullification.supply_shortage = function(self, card, to, from, positive) end end else - if self:isEnemie(to) then + if self:isEnemy(to) then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -176,3 +177,4 @@ fk.ai_skill_invoke["#fan_skill"] = function(self) end end end +--]] diff --git a/packages/standard/ai/aux_skills.lua b/packages/standard/ai/aux_skills.lua index e116f34c..bbcabfee 100644 --- a/packages/standard/ai/aux_skills.lua +++ b/packages/standard/ai/aux_skills.lua @@ -5,70 +5,51 @@ ----------------------------- --- 弃牌相关判定函数的表。键为技能名,值为原型如下的函数。 ----@type table +---@type table fk.ai_discard = {} ---- 请求弃置 ---- ----由skillName进行下一级的决策,只需要在下一级里返回需要弃置的卡牌id表就行 -fk.ai_use_skill["discard_skill"] = function(self, prompt, cancelable, data) - local ask = fk.ai_discard[data.skillName] - self:assignValue() - if type(ask) == "function" then - ask = ask(self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) +local default_discard = function(self, min_num, num, include_equip, cancelable, pattern, prompt) + if cancelable then return nil end + local flag = "h" + if include_equip then + flag = "he" end - if type(ask) ~= "table" and not cancelable then - local flag = "h" - if data.include_equip then - flag = "he" - end - ask = {} - local cards = table.map(self.player:getCardIds(flag), function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, c in ipairs(cards) do - table.insert(ask, c.id) - if #ask >= data.min_num then - break - end + local ret = {} + local cards = self.player:getCardIds(flag) + for _, cid in ipairs(cards) do + table.insert(ret, cid) + if #ret >= min_num then + break end end - if type(ask) == "table" and #ask >= data.min_num then - self.use_id = json.encode { - skill = data.skillName, - subcards = ask - } - end + return ret +end + +fk.ai_active_skill["discard_skill"] = function(self, prompt, cancelable, data) + local ret = self:callFromTable(fk.ai_discard, not cancelable and default_discard, data.skillName, + self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) + + if ret == nil or #ret < data.min_num then return nil end + + return self:buildUseReply { skill = "discard_skill", subcards = ret } end -- choose_players_skill: 选人相关AI ------------------------------------- +---@class ChoosePlayersReply +---@field cardId integer|nil +---@field targets integer[] + --- 选人相关判定函数的表。键为技能名,值为原型如下的函数。 ----@type table +---@type table fk.ai_choose_players = {} ---- 请求选择目标 ---- ----由skillName进行下一级的决策,只需要在下一级里给self.use_tos添加角色id为目标就行 -fk.ai_use_skill["choose_players_skill"] = function(self, prompt, cancelable, data) - local ask = fk.ai_choose_players[data.skillName] - if type(ask) == "function" then - ask(self, data.targets, data.min_num, data.num, cancelable) - end - if #self.use_tos > 0 then - if self.use_id then - self.use_id = json.encode { - skill = data.skillName, - subcards = self.use_id - } - else - self.use_id = json.encode { - skill = data.skillName, - subcards = {} - } - end +fk.ai_active_skill["choose_players_skill"] = function(self, prompt, cancelable, data) + local ret = self:callFromTable(fk.ai_choose_players, nil, data.skillName, + self, data.targets, data.min_num, data.num, cancelable) + + if ret then + return self:buildUseReply({ skill = "choose_players_skill", subcards = { ret.cardId } }, ret.targets) end end diff --git a/packages/standard/ai/init.lua b/packages/standard/ai/init.lua index 2061d45a..04a7ab47 100644 --- a/packages/standard/ai/init.lua +++ b/packages/standard/ai/init.lua @@ -1,5 +1,6 @@ require "packages.standard.ai.aux_skills" +--[[ fk.ai_use_play["rende"] = function(self, skill) for _, p in ipairs(self.friends_noself) do if p.kingdom == "shu" and #self.player:getCardIds("h") >= self.player.hp then @@ -194,7 +195,7 @@ fk.ai_skill_invoke["tieqi"] = function(self, data, prompt) local use = self:eventData("UseCard") for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do p = self.room:getPlayerById(p) - if self:isEnemie(p) then + if self:isEnemy(p) then return true end end @@ -215,13 +216,13 @@ fk.ai_skill_invoke["biyue"] = true fk.ai_choose_players["tuxi"] = function(self, targets, min_num, num, cancelable) for _, pid in ipairs(targets) do local p = self.room:getPlayerById(pid) - if self:isEnemie(p) and #self.use_tos < num then + if self:isEnemy(p) and #self.use_tos < num then table.insert(self.use_tos, pid) end end end -fk.ai_use_skill["yiji_active"] = function(self, prompt, cancelable, data) +fk.ai_active_skill["yiji_active"] = function(self, prompt, cancelable, data) for _, p in ipairs(self.friends_noself) do for c, cid in ipairs(self.player.tag["yiji_ids"]) do c = Fk:getCardById(cid) @@ -247,7 +248,7 @@ fk.ai_choose_players["liuli"] = function(self, targets, min_num, num, cancelable self:sortValue(cards) for _, pid in ipairs(targets) do local p = self.room:getPlayerById(pid) - if self:isEnemie(p) and #self.use_tos < num and #cards > 0 then + if self:isEnemy(p) and #self.use_tos < num and #cards > 0 then table.insert(self.use_tos, pid) self.use_id = { cards[1].id } return @@ -262,3 +263,4 @@ fk.ai_choose_players["liuli"] = function(self, targets, min_num, num, cancelable end end end +--]] diff --git a/packages/standard_cards/ai/init.lua b/packages/standard_cards/ai/init.lua index a2904e48..4fb90903 100644 --- a/packages/standard_cards/ai/init.lua +++ b/packages/standard_cards/ai/init.lua @@ -1,3 +1,38 @@ +-- 基本牌:杀,闪,桃 + +fk.ai_use_card["slash"] = function(self, pattern, prompt, cancelable, extra_data) + local slashes = self:getCards("slash", "use", extra_data) + if #slashes == 0 then return nil end + + -- TODO: 目标合法性 + local targets = {} + if self.enemies[1] then table.insert(targets, self.enemies[1].id) end + + return self:buildUseReply(slashes[1].id, targets) +end + +fk.ai_use_card["peach"] = function(self, _, _, _, extra_data) + local cards = self:getCards("peach", "use", extra_data) + if #cards == 0 then return nil end + + return self:buildUseReply(cards[1].id) +end + +-- 自救见军争卡牌AI +fk.ai_use_card["#AskForPeaches"] = function(self) + local room = self.room + local deathEvent = room.logic:getCurrentEvent() + local data = deathEvent.data[1] ---@type DyingStruct + + -- TODO: 关于救不回来、神关羽之类的更复杂逻辑 + -- TODO: 这些逻辑感觉不能写死在此函数里面,得想出更加多样的办法 + if self:isFriend(room:getPlayerById(data.who)) then + return fk.ai_use_card["peach"](self) + end + return nil +end + +--[[ fk.ai_card.slash = { intention = 100, -- 身份值 value = 4, -- 卡牌价值 @@ -101,7 +136,7 @@ fk.ai_use_play["slash"] = function(self, card) end end -fk.ai_ask_usecard["#slash-jink"] = function(self, pattern, prompt, cancelable, extra_data) +fk.ai_use_card["#slash-jink"] = function(self, pattern, prompt, cancelable, extra_data) local act = self:getActives(pattern) if tonumber(prompt:split(":")[4]) > #act then return @@ -138,7 +173,7 @@ fk.ai_ask_usecard["#slash-jink"] = function(self, pattern, prompt, cancelable, e end end -fk.ai_ask_usecard["#slash-jinks"] = fk.ai_ask_usecard["#slash-jink"] +fk.ai_use_card["#slash-jinks"] = fk.ai_use_card["#slash-jink"] fk.ai_use_play["snatch"] = function(self, card) for _, p in ipairs(self.friends_noself) do @@ -164,7 +199,7 @@ fk.ai_nullification.snatch = function(self, card, to, from, positive) end end else - if self:isEnemie(to) and self:isEnemie(from) then + if self:isEnemy(to) and self:isEnemy(from) then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -196,7 +231,7 @@ fk.ai_nullification.dismantlement = function(self, card, to, from, positive) end end else - if self:isEnemie(to) and self:isEnemie(from) then + if self:isEnemy(to) and self:isEnemy(from) then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -222,7 +257,7 @@ fk.ai_nullification.indulgence = function(self, card, to, from, positive) end end else - if self:isEnemie(to) then + if self:isEnemy(to) then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -261,7 +296,7 @@ end fk.ai_nullification.collateral = function(self, card, to, from, positive) if positive then - if self:isFriend(to) and self:isEnemie(from) then + if self:isFriend(to) and self:isEnemy(from) then if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then self.use_id = self.avail_cards[1] end @@ -271,7 +306,7 @@ end fk.ai_nullification.ex_nihilo = function(self, card, to, from, positive) if positive then - if self:isEnemie(to) then + if self:isEnemy(to) then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -293,7 +328,7 @@ fk.ai_nullification.savage_assault = function(self, card, to, from, positive) end end else - if self:isEnemie(to) then + if self:isEnemy(to) then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -309,7 +344,7 @@ fk.ai_nullification.archery_attack = function(self, card, to, from, positive) end end else - if self:isEnemie(to) then + if self:isEnemy(to) then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -319,7 +354,7 @@ end fk.ai_nullification.god_salvation = function(self, card, to, from, positive) if positive then - if self:isEnemie(to) and to.hp ~= to.maxHp then + if self:isEnemy(to) and to.hp ~= to.maxHp then if #self.avail_cards > 1 or self:isWeak(to) then self.use_id = self.avail_cards[1] end @@ -410,7 +445,7 @@ end fk.ai_discard["#double_swords_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) local use = self:eventData("UseCard") - return self:isEnemie(use.from) and { self.player:getCardIds("h")[1] } + return self:isEnemy(use.from) and { self.player:getCardIds("h")[1] } end fk.ai_discard["#axe_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) @@ -420,7 +455,7 @@ fk.ai_discard["#axe_skill"] = function(self, min_num, num, include_equip, cancel if Fk:getCardById(cid):matchPattern(pattern) then table.insert(ids, cid) end - if #ids >= min_num and self:isEnemie(effect.to) + if #ids >= min_num and self:isEnemy(effect.to) and (self:isWeak(effect.to) or #self.player:getCardIds("he") > 3) then return ids end @@ -436,3 +471,4 @@ fk.ai_skill_invoke["#eight_diagram_skill"] = function(self) local effect = self:eventData("CardEffect") return effect and self:isFriend(effect.to) end +--]]