-- SPDX-License-Identifier: GPL-3.0-or-later -- Trust AI ---@class TrustAI: AI TrustAI = AI:subclass("TrustAI") ---@param self TrustAI ---@param skill ActiveSkill|ViewAsSkill|Card 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 fk.ai_use_play = {} local trust_cb = {} trust_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 fk.ai_use_skill = {} 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 end end fk.ai_choose_players = {} fk.ai_use_skill.discard_skill = function(self, prompt, cancelable, data) local ask = fk.ai_dis_card[data.skillName] self:assignValue() if type(ask) == "function" then ask = ask(self, data.min_num, data.num, data.include_equip, cancelable, data.pattern, prompt) 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 end end if type(ask) == "table" and #ask >= data.min_num then self.use_id = json.encode { skill = data.skillName, subcards = ask } end end fk.ai_dis_card = {} trust_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 fk.ai_skill_invoke = {} trust_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_askuse_card[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_askuse_card[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 fk.ai_askuse_card = {} fk.ai_nullification = {} fk.ai_askuse_card.nullification = function(self, pattern, prompt, cancelable, extra_data) local effect = self:eventData("CardEffect") if effect.to then fk.askNullificationData = effect fk.askNullification = 1 elseif effect.from then fk.askNullification = fk.askNullification + 1 end effect = fk.askNullificationData local positive = fk.askNullification % 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 fk.ai_askuse_card["#AskForPeaches"] = function(self, pattern, prompt, cancelable, extra_data) local dying = self:eventData("Dying") local who = self.room:getPlayerById(dying.who) if who and self:isFriend(who) 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_askuse_card["#AskForPeachesSelf"] = fk.ai_askuse_card["#AskForPeaches"] fk.ai_card = {} fk.cardValue = {} function TrustAI: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 function TrustAI: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 return v end function TrustAI: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 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 } function TrustAI:sort(players, key, inverse) 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 inverse then players = table.reverse(players) end end function TrustAI:sortValue(cards, inverse) local function compare_func(a, b) return self:getValue(a) < self:getValue(b) end table.sort(cards, compare_func) if inverse then cards = table.reverse(cards) end end function TrustAI:sortPriority(cards, inverse) 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 inverse then cards = table.reverse(cards) end end ---@param self TrustAI trust_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) end local effect = self:eventData("CardEffect") if effect and effect.card then self:setUseId(pattern) end end if self.use_id then return json.encode { card = self.use_id, targets = {} } end return "" end fk.ai_response_card = {} function TrustAI:getRetrialCardId(cards, exchange) local judge = self:eventData("Judge") local isgood = judge.card:matchPattern(judge.pattern) 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(judge.pattern) or self:isEnemie(judge.who) and not c:matchPattern(judge.pattern) then table.insert(canRetrial, c) end end if #canRetrial > 0 then return canRetrial[1].id end end function TrustAI: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 function TrustAI: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 function TrustAI: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 ---@param self TrustAI trust_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 fk.ai_card_chosen = {} trust_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]) 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 for c, id in ipairs(to:getCardIds("e")) do --c = Fk:getCardById(id) return id 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 fk.ai_role = {} fk.roleValue = {} fk.trick_judge = {} fk.trick_judge.indulgence = ".|.|heart" fk.trick_judge.lightning = ".|.|^spade" fk.trick_judge.supply_shortage = ".|.|club" local function table_clone(self) local t = {} for _, r in ipairs(self) do table.insert(t, r) end return t end trust_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 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 i, j in ipairs(table.reverse(jt)) do local tj = fk.trick_judge[j.name] if tj then for _, c in ipairs(table_clone(cards)) do if c:matchPattern(tj) 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 function TrustAI:initialize(player) AI.initialize(self, player) self.cb_table = trust_cb self.player = player self.room = RoomInstance or ClientInstance fk.ai_role[player.id] = "neutral" fk.roleValue[player.id] = { lord = 0, loyalist = 0, rebel = 0, renegade = 0 } self:updatePlayers() end function TrustAI:isRolePredictable() return self.room.settings.gameMode ~= "aaa_role_mode" end local function aliveRoles(room) fk.alive_roles = { lord = 0, loyalist = 0, rebel = 0, renegade = 0 } for _, ap in ipairs(room:getAllPlayers(false)) do fk.alive_roles[ap.role] = 0 end for _, ap in ipairs(room:getAlivePlayers(false)) do fk.alive_roles[ap.role] = fk.alive_roles[ap.role] + 1 end return fk.alive_roles end function TrustAI:objectiveLevel(to) if self.player.id == to.id then return -2 elseif #self.room:getAlivePlayers(false) < 3 then return 5 end local ars = aliveRoles(self.room) if self:isRolePredictable() then fk.ai_role[self.player.id] = self.role fk.roleValue[self.player.id][self.role] = 666 if self.role == "renegade" then fk.explicit_renegade = true end for _, p in ipairs(self.room:getAlivePlayers()) do if p.role == self.role or p.role == "lord" and self.role == "loyalist" or p.role == "loyalist" and self.role == "lord" then table.insert(self.friends, p) if p.id ~= self.player.id then table.insert(self.friends_noself, p) end else table.insert(self.enemies, p) end end elseif self.role == "renegade" then if to.role == "lord" then return -1 elseif ars.rebel < 1 then return 4 elseif fk.ai_role[to.id] == "loyalist" then return ars.lord + ars.loyalist - ars.rebel elseif fk.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 fk.ai_role[to.id] == "rebel" then return 5 elseif to.role == "lord" then return -2 elseif ars.rebel < 1 then if self.role == "lord" then return fk.explicit_renegade and fk.ai_role[to.id] == "renegade" and 4 or to.hp > 1 and 2 or 0 elseif fk.explicit_renegade then return fk.ai_role[to.id] == "renegade" and 4 or -1 else return 3 end elseif fk.ai_role[to.id] == "loyalist" then return -2 elseif fk.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 fk.ai_role[to.id] == "loyalist" then return 4 elseif fk.ai_role[to.id] == "rebel" then return -2 elseif fk.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 return 0 end function TrustAI:updatePlayers(update) self.role = self.player.role local neutrality = {} self.enemies = {} self.friends = {} self.friends_noself = {} local aps = self.room:getAlivePlayers() local function compare_func(a, b) local v1 = fk.roleValue[a.id].rebel local v2 = fk.roleValue[b.id].rebel if v1 == v2 then v1 = fk.roleValue[a.id].renegade v2 = fk.roleValue[b.id].renegade end return v1 > v2 end table.sort(aps, compare_func) fk.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 fk.ai_role[ap.id] = "loyalist" elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then rebel = rebel + 1 fk.ai_role[ap.id] = "rebel" elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then renegade = renegade + 1 fk.ai_role[ap.id] = "renegade" fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then loyalist = loyalist + 1 fk.ai_role[ap.id] = "loyalist" else fk.ai_role[ap.id] = "neutral" end end for n, p in ipairs(self.room:getAlivePlayers(false)) 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 fk.roleValue[player.id].rebel > 0 and intention < 0 then fk.roleValue[player.id].renegade = fk.roleValue[player.id].renegade + intention * (100 - fk.roleValue[player.id].renegade) / 200 end local aps = player.room:getAlivePlayers() local function compare_func(a, b) local v1 = fk.roleValue[a.id].rebel local v2 = fk.roleValue[b.id].rebel if v1 == v2 then v1 = fk.roleValue[a.id].renegade v2 = fk.roleValue[b.id].renegade end return v1 > v2 end table.sort(aps, compare_func) fk.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 fk.ai_role[ap.id] = "loyalist" elseif fk.roleValue[ap.id].rebel > 50 and ars.rebel > rebel then rebel = rebel + 1 fk.ai_role[ap.id] = "rebel" elseif fk.roleValue[ap.id].renegade > 50 and ars.renegade > renegade then renegade = renegade + 1 fk.ai_role[ap.id] = "renegade" fk.explicit_renegade = fk.roleValue[ap.id].renegade > 100 elseif fk.roleValue[ap.id].rebel < -50 and ars.loyalist > loyalist then loyalist = loyalist + 1 fk.ai_role[ap.id] = "loyalist" else fk.ai_role[ap.id] = "neutral" end end fk.qWarning( player.general .. " " .. intention .. " " .. fk.ai_role[player.id] .. " rebelValue:" .. fk.roleValue[player.id].rebel .. " renegadeValue:" .. fk.roleValue[player.id].renegade ) --]] end end function TrustAI:filterEvent(event, player, data) 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 = self.room:getPlayerById(p) local intention = callback(p.ai, data.card, self.room:getPlayerById(data.from)) if type(intention) == "number" then updateIntention(self.room:getPlayerById(data.from), p, intention) end end elseif type(callback) == "number" then for _, p in ipairs(TargetGroup:getRealTargets(data.tos)) do p = self.room:getPlayerById(p) updateIntention(self.room:getPlayerById(data.from), p, callback) end end elseif event == fk.StartJudge then fk.trick_judge[data.reason] = data.pattern elseif event == fk.AfterCardsMove then end end function TrustAI: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 function TrustAI:isFriend(pid, tid) if tid then local bt = self:isFriend(pid) return bt ~= nil and bt == self:isFriend(tid) end if type(pid) == "number" then pid = self.room:getPlayerById(pid) end local ve = self:objectiveLevel(pid) if ve < 0 then return true elseif ve > 0 then return false end end function TrustAI:isEnemie(pid, tid) if tid then local bt = self:isFriend(pid) return bt ~= nil and bt ~= self:isFriend(tid) end if type(pid) == "number" then pid = self.room:getPlayerById(pid) end local ve = self:objectiveLevel(pid) if ve > 0 then return true elseif ve < 0 then return false end end function TrustAI:eventData(game_event) local event = self.room.logic:getCurrentEvent():findParent(GameEvent[game_event], true) return event and event.data[1] end for _, n in ipairs(FileIO.ls("packages")) do if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then dofile("packages/" .. n .. "/" .. n .. "_ai.lua") end end -- 加载两次拓展是为了能够引用,例如属性杀的使用直接套入普通杀的使用 for _, n in ipairs(FileIO.ls("packages")) do if FileIO.isDir("packages/" .. n) and FileIO.exists("packages/" .. n .. "/" .. n .. "_ai.lua") then dofile("packages/" .. n .. "/" .. n .. "_ai.lua") end end return TrustAI