From e070f9d3c2b5b89a3c6d5c7e2b4c52f48aff90ab Mon Sep 17 00:00:00 2001 From: notify Date: Mon, 28 Oct 2024 17:17:09 +0800 Subject: [PATCH 1/5] =?UTF-8?q?smart-ai=E4=B9=8B=E6=9C=80=E7=AE=80?= =?UTF-8?q?=E5=8D=95=E5=87=BA=E7=89=8C=E7=AD=96=E7=95=A5=E4=B8=8EAPI?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lua/core/util.lua | 7 +- lua/server/ai/ai.lua | 37 +++++ lua/server/ai/init.lua | 19 ++- lua/server/ai/smart_ai.lua | 278 +++++++++--------------------------- lua/server/network.lua | 5 +- lua/server/serverplayer.lua | 2 +- maneuvering/ai/init.lua | 185 +----------------------- standard/ai/init.lua | 6 +- standard_cards/ai/init.lua | 124 +++++----------- 9 files changed, 173 insertions(+), 490 deletions(-) diff --git a/lua/core/util.lua b/lua/core/util.lua index 77ffd36..0a27025 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -98,15 +98,15 @@ end ---@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 t_vals = table.map(t2, val_func or function(e) return e end) 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) + max_idx, max, max_val = i, v, t_vals[i] else - local val = val_func(v) + local val = t_vals[i] local checked = val > max_val if reverse then checked = not checked end if checked then @@ -116,6 +116,7 @@ function fk.sorted_pairs(t, val_func, reverse) end if max_idx == -1 then return nil, nil end table.remove(t2, max_idx) + table.remove(t_vals, max_idx) return -1, max, max_val end return iter, nil, 1 diff --git a/lua/server/ai/ai.lua b/lua/server/ai/ai.lua index 36ed560..9d11f6d 100644 --- a/lua/server/ai/ai.lua +++ b/lua/server/ai/ai.lua @@ -79,6 +79,17 @@ function AI:getSelectedCards() return self.handler.pendings end +---@return Card? +function AI:getSelectedCard() + if not self:isInDashboard() then return Util.DummyTable end + local handler = self.handler + if handler.selected_card then return handler.selected_card end + if not handler.skill_name then return end + local skill = Fk.skills[handler.skill_name] + if not skill:isInstanceOf(ViewAsSkill) then return end + return skill:viewAs(handler.pendings) +end + ---@return ServerPlayer[] function AI:getSelectedTargets() if not self:isInDashboard() then return Util.DummyTable end @@ -108,6 +119,30 @@ function AI:selectSkill(skill_name, selected) self.handler:update("SkillButton", skill_name, "click", { selected = selected }) end +function AI:unSelectAllCards() + for _, id in ipairs(self:getSelectedCards()) do + self:selectCard(id, false) + end +end + +function AI:unSelectAllTargets() + for _, p in ipairs(self:getSelectedTargets()) do + self:selectTarget(p, false) + end +end + +function AI:unSelectSkill() + local skill = self:getSelectedSkill() + if not skill then return end + self:selectSkill(skill, false) +end + +function AI:unSelectAll() + self:unSelectSkill() + self:unSelectAllCards() + self:unSelectAllTargets() +end + function AI:okButtonEnabled() if not self:isInDashboard() then return false end return self.handler:feasible() @@ -121,6 +156,7 @@ end function AI:makeReply() Self = self.player + -- local now = os.getms() local fn = self["handle" .. self.command] local ret = "__cancel" if fn then @@ -133,6 +169,7 @@ function AI:makeReply() end if ret == "" then ret = "__cancel" end self.handler = nil + -- printf("%s 在%fms后得出结果:%s", self.command, (os.getms() - now) / 1000, json.encode(ret)) return ret end diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 07e5699..60b1a7f 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -4,18 +4,27 @@ AI = require "server.ai.ai" TrustAI = require "server.ai.trust_ai" RandomAI = require "server.ai.random_ai" ---[[ 在release版暂时不启动。 +---[[ 在release版暂时不启动。 SmartAI = require "server.ai.smart_ai" ---[[ 调试中,暂且不加载额外的AI。 -- load ai module from packages -local directories = FileIO.ls("packages") -require "packages.standard.ai" -require "packages.standard_cards.ai" -require "packages.maneuvering.ai" +local directories +if UsingNewCore then + directories = FileIO.ls("..") + -- require "standard.ai" + require "standard_cards.ai" + -- require "maneuvering.ai" +else + directories = FileIO.ls("packages") + require "packages.standard.ai" + require "packages.standard_cards.ai" + require "packages.maneuvering.ai" +end table.removeOne(directories, "standard") table.removeOne(directories, "standard_cards") table.removeOne(directories, "maneuvering") +--[[ local _disable_packs = json.decode(fk.GetDisabledPacks()) for _, dir in ipairs(directories) do diff --git a/lua/server/ai/smart_ai.lua b/lua/server/ai/smart_ai.lua index b102003..166414e 100644 --- a/lua/server/ai/smart_ai.lua +++ b/lua/server/ai/smart_ai.lua @@ -18,24 +18,19 @@ -- TODO: 更加详细的文档 --]] ----@class SmartAI: AI +---@class SmartAI: TrustAI ---@field private _memory table @ AI底层的空间换时间机制 ---@field public friends ServerPlayer[] @ 队友 ---@field public enemies ServerPlayer[] @ 敌人 -local SmartAI = AI:subclass("SmartAI") - ----@type table -local smart_cb = {} +local SmartAI = TrustAI:subclass("SmartAI") -- 哦,我懒得写出闪之类的,不得不继承一下,饶了我吧 function SmartAI:initialize(player) - AI.initialize(self, player) - self.cb_table = smart_cb - self.player = player + TrustAI.initialize(self, player) end function SmartAI:makeReply() self._memory = {} - return AI.makeReply(self) + return TrustAI.makeReply(self) end function SmartAI:__index(k) @@ -72,183 +67,79 @@ end -- 面板相关交互:对应操控手牌区、技能面板、直接选择目标的交互 -- 对应UI中的"responding"状态和"playing"状态 -- AI代码需要像实际操作UI那样完成以下几个任务: --- * 点击技能按钮(出牌阶段或者打算使用ViewAsSkill) --- * 技能如果带有interaction,则选择interaction --- * 如果需要的话点选手牌 +-- * 点击技能按钮,完成interaction与子卡选择;或者直接点可用手牌 -- * 选择目标 -- * 点确定 --- 这些步骤归结起来,就是让AI想办法返回如下定义的UseReply --- 或者返回nil表示点取消 --=================================================== ----@class UseReply ----@field card? integer|string @ string情况下是json.encode后 ----@field targets? integer[] ----@field special_skill string @ 出牌阶段空闲点使用实体卡特有 ----@field interaction_data any @ 因技能而异,一般都是nil - ----@param card integer|table ----@param targets? integer[] ----@param special_skill? string ----@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] - skill._extra_data = extra_data - - 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 = setmetatable({}, { - __index = function(_, k) - -- FIXME: 感觉不妥 - local c = Fk.all_card_types[k] - if not c then return nil end - if c.type == Card.TypeEquip then - return function(self, pattern, prompt, cancelable, extra_data) - local slashes = self:getCards(k, "use", extra_data) - if #slashes == 0 then return nil end - - return self:buildUseReply(slashes[1].id) - end - end - end, -}) - -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) +--- 主动技/视为技在SmartAI中几个选牌选目标的函数 +---@class SmartAISkillSpec +---@field name string +---@field will_use? fun(skill: ActiveSkill, ai: SmartAI, card: Card): boolean? +---@field choose_interaction? fun(skill: ActiveSkill, ai: SmartAI): boolean? +---@field choose_cards? fun(skill: ActiveSkill, ai: SmartAI): boolean? +---@field choose_targets fun(skill: ActiveSkill, ai: SmartAI, card: Card?): any + +---@type table +fk.ai_skills = {} + +---@param spec SmartAISkillSpec +function SmartAI.static:registerActiveSkill(spec, key) + spec.will_use = spec.will_use or Util.FalseFunc + -- spec.choose_interaction = spec.choose_interaction or Util.FalseFunc + -- spec.choose_cards = spec.choose_cards or Util.FalseFunc + spec.choose_targets = spec.choose_targets or Util.FalseFunc + fk.ai_skills[key or spec.name] = spec 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) +---@param spec SmartAISkillSpec +---@param key string? 可以指定不同于spec.name的key 以便复用 +---@diagnostic disable-next-line +function SmartAI:registerActiveSkill(spec, key) + error("This is a static method. Please use SmartAI:registerActiveSkill(...)") 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: 视为技 - -- 视为技对应的function一般会返回一张印出来的卡,又要纳入新的考虑范围了 - -- 不过这种根据牌名判断的逻辑而言 可能需要调用多次视为技函数了 - -- 要用好空间换时间 - 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) +function SmartAI:handlePlayCard() + local to_use = self:getEnabledCards() + table.insertTable(to_use, self:getEnabledSkills()) + + -- local value_func = function(str) return 1 end + -- for _, id_or_skill in fk.sorted_pairs(to_use, value_func) do + for _, id_or_skill in ipairs(to_use) do + local skill + local card + + if type(id_or_skill) == "string" then + skill = Fk.skills[id_or_skill] --[[@as ActiveSkill]] + else + card = Fk:getCardById(id_or_skill) + skill = card.skill + end + + local spec = fk.ai_skills[skill.name] + if spec and spec.will_use(skill, self, card) then + if not card then + self:selectSkill(id_or_skill, true) + if spec.choose_interaction and not spec.choose_interaction(skill, self) then + goto continue + end + if spec.choose_cards and not spec.choose_cards(skill, self) then + goto continue + end + card = self:getSelectedCard() -- 可能nil 但总之也要进入最终选目标阶段 + else + self:selectCard(id_or_skill, true) + end - if ret then return json.encode(ret) end + local ret = spec.choose_targets(skill, self, card) + if ret and ret ~= "" then return ret end end + + ::continue:: + self:unSelectAll() end return "" @@ -265,11 +156,10 @@ end -- 函数返回true或者false即可。 ----------------------------- ----@type table +---@type table fk.ai_skill_invoke = {} -smart_cb["AskForSkillInvoke"] = function(self, jsonData) - local data = json.decode(jsonData) +function SmartAI:handleAskForSkillInvoke(data) local skillName, prompt = data[1], data[2] local ask = fk.ai_skill_invoke[skillName] @@ -280,7 +170,7 @@ smart_cb["AskForSkillInvoke"] = function(self, jsonData) elseif Fk.skills[skillName].frequency == Skill.Frequent then return "1" else - return RandomAI.cb_table["AskForSkillInvoke"](self, jsonData) + return math.random() < 0.5 and "1" or "" end end @@ -309,38 +199,4 @@ end -- 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, - response = Player.prohibitResponse, - discard = Player.prohibitDiscard, -} - ---- 根据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) - - 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) - - -- TODO: 考虑视为技,这里可以再返回一些虚拟牌 - return ret -end - return SmartAI diff --git a/lua/server/network.lua b/lua/server/network.lua index 9eac1b4..75098b4 100644 --- a/lua/server/network.lua +++ b/lua/server/network.lua @@ -299,8 +299,7 @@ end function Request:_finish() local room = self.room surrenderCheck(room) - -- FIXME: 这里QML中有个bug,这个命令应该是用来暗掉玩家面板的 - -- room:doBroadcastNotify("CancelRequest", "") + for _, p in ipairs(self.players) do p.serverplayer:setThinking(false) -- 这个什么timewaste_count也该扔了 @@ -327,7 +326,7 @@ function Request:_finish() for _, isHuman in pairs(self.send_success) do if not self.ai_start_time then break end if not isHuman then - local to_delay = 500 - (os.getms() - self.ai_start_time) / 1000 + local to_delay = 800 - (os.getms() - self.ai_start_time) / 1000 room:delay(to_delay) break end diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 28fef5e..67a1b94 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -40,7 +40,7 @@ function ServerPlayer:initialize(_self) self._prelighted_skills = {} self._timewaste_count = 0 - self.ai = TrustAI:new(self) + self.ai = SmartAI:new(self) end ---@param command string diff --git a/maneuvering/ai/init.lua b/maneuvering/ai/init.lua index 7785abb..9ebcc10 100644 --- a/maneuvering/ai/init.lua +++ b/maneuvering/ai/init.lua @@ -1,180 +1,7 @@ ---[[ -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 -fk.ai_use_play.fire__slash = fk.ai_use_play.slash -fk.ai_card.analeptic = { - intention = 60, -- 身份值 - value = 5, -- 卡牌价值 - priority = 3 -- 使用优先值 -} +SmartAI:registerActiveSkill(fk.ai_skills["slash_skill"], "thunder__slash_skill") +SmartAI:registerActiveSkill(fk.ai_skills["slash_skill"], "fire__slash_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "analeptic_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "iron_chain_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__use_to_enemy"], "fire_attack_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__use_to_enemy"], "supply_shortage_skill") -fk.ai_use_play["analeptic"] = function(self, card) - local cards = table.map(self.player:getCardIds("&he"), function(id) - return Fk:getCardById(id) - end) - self:sortValue(cards) - for _, sth in ipairs(self:getActives("slash")) do - local slash = nil - if sth:isInstanceOf(Card) then - if sth.skill:canUse(self.player, sth) and not self.player:prohibitUse(sth) then - slash = sth - 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("slash") and tc.skill:canUse(self.player, tc) and not self.player:prohibitUse(tc) then - slash = tc - end - end - if slash then - fk.ai_use_play.slash(self, slash) - if self.use_id then - self.use_id = card.id - self.use_tos = {} - break - end - end - end -end - -fk.ai_card.iron_chain = { - intention = function(self, card, from) - if self.player.chained then - return -80 - end - return 80 - end, -- 身份值 - value = 2, -- 卡牌价值 - priority = 3 -- 使用优先值 -} - -fk.ai_use_play["iron_chain"] = function(self, card) - for _, p in ipairs(self.friends) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and p.chained then - table.insert(self.use_tos, p.id) - end - end - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and not p.chained then - table.insert(self.use_tos, p.id) - end - end - if #self.use_tos < 2 then - self.use_tos = {} - else - self.use_id = card.id - end -end - -fk.ai_use_play["recast"] = function(self, card) - if self.command == "PlayCard" then - self.use_id = card.id - self.special_skill = "recast" - end -end - -fk.ai_card.fire_attack = { - intention = 90, -- 身份值 - value = 3, -- 卡牌价值 - priority = 4 -- 使用优先值 -} - -fk.ai_use_play["fire_attack"] = function(self, card) - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #self.player:getCardIds("h") > 2 then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end -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:isEnemy(p) then - local cards = table.map(self.player:getCardIds("h"), function(id) - return Fk:getCardById(id) - end) - local exp = Exppattern:Parse(pattern) - cards = table.filter(cards, function(c) - return exp:match(c) - end) - if #cards > 0 then - self:sortValue(cards) - return { cards[1].id } - end - end - end -end - -fk.ai_nullification.fire_attack = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) and #to:getCardIds("h") > 0 and #from:getCardIds("h") > 0 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 - end - else - 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 - end - end -end - -fk.ai_card.fire_attack = { - intention = 120, -- 身份值 - value = 2, -- 卡牌价值 - priority = 2 -- 使用优先值 -} - -fk.ai_use_play["supply_shortage"] = function(self, card) - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and not p.chained then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end -end - -fk.ai_nullification.supply_shortage = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) 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 - end - else - if self:isEnemy(to) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_card.supply_shortage = { - intention = 130, -- 身份值 - value = 2, -- 卡牌价值 - priority = 1 -- 使用优先值 -} - -fk.ai_skill_invoke["#fan_skill"] = function(self) - local use = self:eventData("UseCard") - for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do - if not self:isFriend(p) then - return true - end - end -end ---]] diff --git a/standard/ai/init.lua b/standard/ai/init.lua index e25c861..8d55696 100644 --- a/standard/ai/init.lua +++ b/standard/ai/init.lua @@ -1,4 +1,8 @@ -require "packages.standard.ai.aux_skills" +if UsingNewCore then + require "standard.ai.aux_skills" +else + require "packages.standard.ai.aux_skills" +end -- 魏国 diff --git a/standard_cards/ai/init.lua b/standard_cards/ai/init.lua index 9c94733..7960665 100644 --- a/standard_cards/ai/init.lua +++ b/standard_cards/ai/init.lua @@ -1,87 +1,37 @@ --- TODO: 合法性的方便函数 --- TODO: 关于如何选择多个目标 --- TODO: 关于装备牌 - --- 基本牌:杀,闪,桃 - ----@param from ServerPlayer ----@param to ServerPlayer ----@param card Card -local function tgtValidator(from, to, card) - return not from:prohibitUse(card) and - not from:isProhibited(to, card) and - true -- feasible -end - -local function justUse(self, card_name, extra_data) - local slashes = self:getCards(card_name, "use", extra_data) - if #slashes == 0 then return nil end - - return self:buildUseReply(slashes[1].id) -end - ----@param self SmartAI ----@param card_name string -local function useToEnemy(self, card_name, extra_data) - local slashes = self:getCards(card_name, "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) - else - return nil - end - - return self:buildUseReply(slashes[1].id, targets) -end - -fk.ai_use_card["slash"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "slash", extra_data) -end - -fk.ai_use_card["jink"] = function(self, pattern, prompt, cancelable, extra_data) - return justUse(self, "jink", extra_data) -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_use_card["dismantlement"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "dismantlement", extra_data) -end - -fk.ai_use_card["snatch"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "snatch", extra_data) -end - -fk.ai_use_card["duel"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "duel", extra_data) -end - -fk.ai_use_card["ex_nihilo"] = function(self, pattern, prompt, cancelable, extra_data) - return justUse(self, "ex_nihilo", extra_data) -end - -fk.ai_use_card["indulgence"] = function(self, pattern, prompt, cancelable, extra_data) - return useToEnemy(self, "indulgence", extra_data) -end +SmartAI:registerActiveSkill { + name = "__just_use", + will_use = Util.TrueFunc, + choose_targets = function(skill, ai, card) + return ai:doOKButton() + end, +} + +SmartAI:registerActiveSkill { + name = "__use_to_enemy", + will_use = Util.TrueFunc, + choose_targets = function(skill, ai, card) + local targets = ai:getEnabledTargets() + for _, p in ipairs(targets) do + if ai:isEnemy(p) then + ai:selectTarget(p, true) + break + end + end + return ai:doOKButton() + end, +} + +SmartAI:registerActiveSkill(fk.ai_skills["__use_to_enemy"], "slash_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__use_to_enemy"], "dismantlement_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__use_to_enemy"], "snatch_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__use_to_enemy"], "duel_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__use_to_enemy"], "indulgence_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "jink_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "peach_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "ex_nihilo_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "savage_assault_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "archery_attack_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "god_salvation_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "amazing_grace_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "lightning_skill") +SmartAI:registerActiveSkill(fk.ai_skills["__just_use"], "default_equip_skill") -- Gitee From 5998400920cfb53f9961026f67339df1d852d264 Mon Sep 17 00:00:00 2001 From: Mechanel Date: Mon, 28 Oct 2024 21:41:46 +0900 Subject: [PATCH 2/5] MarkEnum.InvalidSkills can use TempMarkSuffix --- lua/client/client_util.lua | 21 ++++++++++++++++++--- lua/server/mark_enum.lua | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 60b0b0e..0c6f8ff 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -457,10 +457,25 @@ function GetSkillData(skill_name) end function GetSkillStatus(skill_name) + local player = Self local skill = Fk.skills[skill_name] - local locked = not skill:isEffectable(Self) - if not locked and type(Self:getMark(MarkEnum.InvalidSkills)) == "table" and table.contains(Self:getMark(MarkEnum.InvalidSkills), skill_name) then - locked = true + local locked = not skill:isEffectable(player) + if not locked then + for mark, value in pairs(player.mark) do + if mark == MarkEnum.InvalidSkills then + if table.contains(value, skill_name) then + locked = true + break + end + elseif mark:startsWith(MarkEnum.InvalidSkills .. "-") and table.contains(value, skill_name) then + for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do + if mark:find(suffix, 1, true) then + locked = true + break + end + end + end + end end return { locked = locked, ---@type boolean diff --git a/lua/server/mark_enum.lua b/lua/server/mark_enum.lua index dd6eba1..a74aca9 100644 --- a/lua/server/mark_enum.lua +++ b/lua/server/mark_enum.lua @@ -29,7 +29,7 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo" MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" ---非锁定技失效 MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" ----失效技能表 +---失效技能表(用``Room:addTableMark``和``Room:removeTableMark``控制) MarkEnum.InvalidSkills = "InvalidSkills" ---不可明置(值为表,m - 主将, d - 副将) MarkEnum.RevealProhibited = "RevealProhibited" -- Gitee From 54a902a0bcdbf9ebe435c53a1baeed391f33181a Mon Sep 17 00:00:00 2001 From: Mechanel Date: Thu, 31 Oct 2024 00:02:19 +0900 Subject: [PATCH 3/5] enhance MarkEnum.InvalidSkills --- lua/client/client_util.lua | 20 +------------------- lua/core/player.lua | 2 +- lua/core/skill.lua | 14 ++++++++++++++ lua/core/skill_type/usable_skill.lua | 4 ++-- lua/server/events/skill.lua | 5 +++++ lua/server/mark_enum.lua | 2 +- lua/server/room.lua | 18 ++++++++++++++++++ 7 files changed, 42 insertions(+), 23 deletions(-) diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 0c6f8ff..9aa8e46 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -459,26 +459,8 @@ end function GetSkillStatus(skill_name) local player = Self local skill = Fk.skills[skill_name] - local locked = not skill:isEffectable(player) - if not locked then - for mark, value in pairs(player.mark) do - if mark == MarkEnum.InvalidSkills then - if table.contains(value, skill_name) then - locked = true - break - end - elseif mark:startsWith(MarkEnum.InvalidSkills .. "-") and table.contains(value, skill_name) then - for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do - if mark:find(suffix, 1, true) then - locked = true - break - end - end - end - end - end return { - locked = locked, ---@type boolean + locked = not skill:isEffectable(player), times = skill:getTimes() } end diff --git a/lua/core/player.lua b/lua/core/player.lua index e9a4496..2f472d3 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -671,7 +671,7 @@ end --- 设定玩家使用特定牌的历史次数。 ---@param cardName string @ 牌名 ----@param num integer @ 次数 +---@param num? integer @ 次数 默认0 ---@param scope? integer @ 历史范围 全为nil意为清空 function Player:setCardUseHistory(cardName, num, scope) if cardName == "" and num == nil and scope == nil then diff --git a/lua/core/skill.lua b/lua/core/skill.lua index cc81fe2..cc15dd4 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -124,6 +124,20 @@ function Skill:isEffectable(player) end end + for mark, value in pairs(player.mark) do -- 耦合 MarkEnum.InvalidSkills ! + if mark == MarkEnum.InvalidSkills then + if table.contains(value, self.name) then + return false + end + elseif mark:startsWith(MarkEnum.InvalidSkills .. "-") and table.contains(value, self.name) then + for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do + if mark:find(suffix, 1, true) then + return false + end + end + end + end + return true end diff --git a/lua/core/skill_type/usable_skill.lua b/lua/core/skill_type/usable_skill.lua index 58e544f..2a7bc8d 100644 --- a/lua/core/skill_type/usable_skill.lua +++ b/lua/core/skill_type/usable_skill.lua @@ -16,7 +16,7 @@ end -- 获得技能的最大使用次数 ---@param player Player @ 使用者 ----@param scope integer @ 考察时机(默认为回合) +---@param scope integer @ 查询历史范围(默认为回合) ---@param card Card @ 卡牌 ---@param to Player @ 目标 ---@return number @ 最大使用次数 @@ -34,7 +34,7 @@ end -- 判断一个角色是否在技能的次数限制内 ---@param player Player @ 使用者 ----@param scope integer @ 考察时机(默认为回合) +---@param scope integer @ 查询历史范围(默认为回合) ---@param card? Card @ 牌,若没有牌,则尝试制造一张虚拟牌 ---@param card_name? string @ 牌名 ---@param to any @ 目标 diff --git a/lua/server/events/skill.lua b/lua/server/events/skill.lua index be30874..d959733 100644 --- a/lua/server/events/skill.lua +++ b/lua/server/events/skill.lua @@ -137,6 +137,11 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) end end + + self:validateSkill(player, actual_skill) + for _, suf in ipairs(MarkEnum.TempMarkSuffix) do + self:validateSkill(player, actual_skill, suf) + end end end else diff --git a/lua/server/mark_enum.lua b/lua/server/mark_enum.lua index a74aca9..d414ddd 100644 --- a/lua/server/mark_enum.lua +++ b/lua/server/mark_enum.lua @@ -29,7 +29,7 @@ MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo" MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" ---非锁定技失效 MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" ----失效技能表(用``Room:addTableMark``和``Room:removeTableMark``控制) +---失效技能表(用``Room:invalidateSkill``和``Room:validateSkill``控制) MarkEnum.InvalidSkills = "InvalidSkills" ---不可明置(值为表,m - 主将, d - 副将) MarkEnum.RevealProhibited = "RevealProhibited" diff --git a/lua/server/room.lua b/lua/server/room.lua index f46e66d..7126c97 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -2964,6 +2964,24 @@ function Room:removeTableMark(sth, mark, value) end end +--- 无效化技能 +---@param player ServerPlayer +---@param skill_name string +---@param temp? string @ 作用范围,``-round`` ``-turn`` ``-phase``或不填 +function Room:invalidateSkill(player, skill_name, temp) + temp = temp and temp or "" + self:addTableMark(player, MarkEnum.InvalidSkills .. temp, skill_name) +end + +--- 有效化技能 +---@param player ServerPlayer +---@param skill_name string +---@param temp? string @ 作用范围,``-round`` ``-turn`` ``-phase``或不填 +function Room:validateSkill(player, skill_name, temp) + temp = temp and temp or "" + self:removeTableMark(player, MarkEnum.InvalidSkills .. temp, skill_name) +end + function Room:__index(k) if k == "room_settings" then return self.settings -- Gitee From ee656fb1dafafbb6b9e53745e38239574347772e Mon Sep 17 00:00:00 2001 From: Mechanel Date: Sat, 9 Nov 2024 15:54:13 +0000 Subject: [PATCH 4/5] update lua/server/events/skill.lua. Signed-off-by: Mechanel --- lua/server/events/skill.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/server/events/skill.lua b/lua/server/events/skill.lua index 427fb7c..62a8ce5 100644 --- a/lua/server/events/skill.lua +++ b/lua/server/events/skill.lua @@ -132,7 +132,6 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil table.insert(losts, true) table.insert(triggers, s) - -- if s.derived_piles then -- for _, pile_name in ipairs(s.derived_piles) do -- table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) -- Gitee From fa9def7374ad6a25606fd37a8d81f4f5d0754b1f Mon Sep 17 00:00:00 2001 From: Mechanel Date: Tue, 26 Nov 2024 21:13:58 +0900 Subject: [PATCH 5/5] fix --- lua/client/i18n/zh_CN.lua | 2 +- lua/core/card.lua | 6 +++--- lua/server/ai/init.lua | 9 +++++++-- lua/server/room.lua | 5 +++-- standard/init.lua | 4 +--- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 0789c86..374ce4f 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -449,7 +449,7 @@ Fk:loadTranslationTable{ --utility ["draw1"] = "摸一张牌", ["draw2"] = "摸两张牌", - ["draw3"] = "摸两张牌", + ["draw3"] = "摸三张牌", ["recover"] = "回复1点体力", ["loseHp"] = "失去1点体力", ["damage1"] = "造成1点伤害", diff --git a/lua/core/card.lua b/lua/core/card.lua index 020a1c5..dd61611 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -239,7 +239,7 @@ function Card:matchPattern(pattern) return Exppattern:Parse(pattern):match(self) end ---- 获取卡牌花色并返回花色文字描述(如 黑桃、红桃、梅花、方块)或者符号(如♠♥♣♦,带颜色)。 +--- 获取卡牌花色并返回花色文字描述(如``spade``黑桃、``heart``红桃、``club``梅花、``diamond``方块)或者符号(如♠♥♣♦,带颜色)。 ---@param symbol? boolean @ 是否以符号形式显示 ---@return string @ 描述花色的字符串 function Card:getSuitString(symbol) @@ -259,7 +259,7 @@ function Card:getSuitString(symbol) return symbol and "log_" .. ret or ret end ---- 获取卡牌颜色并返回点数颜色描述(例如黑色/红色/无色)。 +--- 获取卡牌颜色并返回点数颜色描述(例如``black``黑色/``red``红色/``nocolor``无色)。 ---@return string @ 描述颜色的字符串 function Card:getColorString() local color = self.color @@ -273,7 +273,7 @@ function Card:getColorString() return "unknown" end ---- 获取卡牌类型并返回类型描述(例如基本牌/锦囊牌/装备牌)。 +--- 获取卡牌类型并返回类型描述(例如``basic``基本牌/``trick``锦囊牌/``equip``装备牌)。 function Card:getTypeString() local t = self.type if t == Card.TypeBasic then diff --git a/lua/server/ai/init.lua b/lua/server/ai/init.lua index 22630e0..fa6c20f 100644 --- a/lua/server/ai/init.lua +++ b/lua/server/ai/init.lua @@ -7,12 +7,13 @@ TrustAI = require "server.ai.trust_ai" SmartAI = require "server.ai.smart_ai" -- load ai module from packages -local directories +local directories = {} if UsingNewCore then - directories = FileIO.ls("..") require "standard_cards.ai" require "standard.ai" require "maneuvering.ai" + FileIO.cd("../..") + directories = FileIO.ls("packages") else directories = FileIO.ls("packages") require "packages.standard_cards.ai" @@ -34,3 +35,7 @@ for _, dir in ipairs(directories) do end end + +if UsingNewCore then + FileIO.cd("packages/freekill-core") +end diff --git a/lua/server/room.lua b/lua/server/room.lua index bf7eea7..42ff5e3 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -1275,7 +1275,6 @@ function Room:askForYiji(player, cards, targets, skillName, minNum, maxNum, prom residued_list = residueMap, expand_pile = expand_pile } - -- p(json.encode(residueMap)) while maxNum > 0 and #_cards > 0 do data.max_num = maxNum @@ -1287,7 +1286,9 @@ function Room:askForYiji(player, cards, targets, skillName, minNum, maxNum, prom for _, id in ipairs(give_cards) do table.insert(list[to], id) table.removeOne(_cards, id) - self:setCardMark(Fk:getCardById(id), "@DistributionTo", Fk:translate(self:getPlayerById(to).general)) + local p = self:getPlayerById(to) + self:setCardMark(Fk:getCardById(id), "@DistributionTo", + Fk:translate(p.general == "anjiang" and "seat#" .. tostring(p.seat) or p.general)) end minNum = math.max(0, minNum - #give_cards) maxNum = maxNum - #give_cards diff --git a/standard/init.lua b/standard/init.lua index 7bb6a12..29fb2e4 100644 --- a/standard/init.lua +++ b/standard/init.lua @@ -730,9 +730,7 @@ local keji = fk.CreateTriggerSkill{ and #player.room.logic:getEventsOfScope(GameEvent.RespondCard, 1, PlayCheck, Player.HistoryTurn) == 0 end end, - on_use = function(self, event, target, player, data) - return true - end + on_use = Util.TrueFunc, } local lvmeng = General:new(extension, "lvmeng", "wu", 4) lvmeng:addSkill(keji) -- Gitee