Ai(鸽之) (#289)

鸽了一点新AI
This commit is contained in:
notify 2023-12-03 18:45:25 +08:00 committed by GitHub
parent 041d5835ff
commit eba115a4fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 456 additions and 1338 deletions

20
docs/dev/smart-ai.rst Normal file
View File

@ -0,0 +1,20 @@
关于类似神杀的Smart-AI的实现思路
==================================
AI的目的就是为了响应各种askFor而Smart-ai则是给了玩家自定义askFor策略的接口。
大体框架还是一样的根据command type去选择执行某个通用函数再根据各种参数不断
细化函数执行最后执行Mod开发者的自定义逻辑。
而如何设计这种接口就是要面对的问题了。
神杀智慧1堆积如山的hasSkill
------------------------------
神杀一个突出的问题就是各种hasSkill写死比如判断要不要黑杀某人直接写死hasSkill
仁王盾啥的
神杀智慧2一次性sort所有卡牌/主动技/视为技
--------------------------------------------
如题,这导致每次都要花秒级甚至分钟级别的时间来出一张牌。

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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
--]]

View File

@ -5,70 +5,51 @@
-----------------------------
--- 弃牌相关判定函数的表。键为技能名,值为原型如下的函数。
---@type table<string, fun(self: SmartAI, min_num: number, num: number, include_equip: bool, cancelable: bool, pattern: string, prompt: string): any>
---@type table<string, fun(self: SmartAI, min_num: number, num: number, include_equip: bool, cancelable: bool, pattern: string, prompt: string): integer[]|nil>
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)
end
if type(ask) ~= "table" and not cancelable then
local default_discard = function(self, min_num, num, include_equip, cancelable, pattern, prompt)
if cancelable then return nil end
local flag = "h"
if data.include_equip then
if 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
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
return ret
end
if type(ask) == "table" and #ask >= data.min_num then
self.use_id = json.encode {
skill = data.skillName,
subcards = ask
}
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<string, fun(self: SmartAI, targets: integer[], min_num: number, num: number, cancelable: bool)>
---@type table<string, fun(self: SmartAI, targets: integer[], min_num: number, num: number, cancelable: bool): ChoosePlayersReply|nil>
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

View File

@ -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
--]]

View File

@ -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
--]]