Changelog: v0.4.23

This commit is contained in:
notify 2024-11-09 19:27:41 +08:00
parent f8b67531c0
commit a09a973064
75 changed files with 2300 additions and 1697 deletions

View File

@ -1,6 +1,6 @@
# ChangeLog
## v0.4.21 & v0.4.22
## v0.4.21 & v0.4.22 & v0.4.23
- 修复了确认键亮起时取消键不可用的bug
- lua端的ob属性根本没同步同步一下

View File

@ -6,7 +6,7 @@
cmake_minimum_required(VERSION 3.22)
project(FreeKill VERSION 0.4.22)
project(FreeKill VERSION 0.4.23)
add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\")
find_package(Qt6 REQUIRED COMPONENTS

View File

@ -153,6 +153,10 @@ Flickable {
selectByMouse: false
wrapMode: TextEdit.WordWrap
textFormat: TextEdit.RichText
property var savedtext: []
function clearSavedText() {
savedtext = [];
}
onLinkActivated: (link) => {
if (link === "back") {
text = savedtext.pop();
@ -180,6 +184,7 @@ Flickable {
screenName.text = "";
playerGameData.text = "";
skillDesc.text = "";
skillDesc.clearSavedText();
const id = extra_data.photo.playerid;
if (id === 0) return;

View File

@ -5,7 +5,7 @@ import QtQuick.Layouts
import QtQuick.Controls
import Fk
import Fk.RoomElement
import "RoomLogic.js" as RoomLogic
// import "RoomLogic.js" as RoomLogic
Item {
id: root
@ -390,12 +390,28 @@ Item {
}
onClicked: {
callbacks["LogEvent"]({
type: "PlaySkillSound",
name: name,
general: specific ? detailGeneralCard.name : null, //
i: idx,
});
const skill = name;
const general = specific ? detailGeneralCard.name : null;
let extension;
let path;
let dat;
// try main general
if (general) {
dat = lcall("GetGeneralData", general);
extension = dat.extension;
path = "./packages/" + extension + "/audio/skill/" + skill + "_" + general;
if (Backend.exists(path + ".mp3") || Backend.exists(path + "1.mp3")) {
Backend.playSound(path, idx);
return;
}
}
// finally normal skill
dat = lcall("GetSkillData", skill);
extension = dat.extension;
path = "./packages/" + extension + "/audio/skill/" + skill;
Backend.playSound(path, idx);
}
onPressAndHold: {

View File

@ -341,6 +341,7 @@ Item {
}
lcall("FinishRequestUI");
applyChange({});
}
}
},
@ -1338,7 +1339,12 @@ Item {
photo.state = pdata.state;
photo.selectable = pdata.enabled;
photo.selected = pdata.selected;
})
});
for (let i = 0; i < photoModel.count; i++) {
const item = photos.itemAt(i);
item.targetTip = lcall("GetTargetTip", item.playerid);
}
const buttons = uiUpdate["Button"];
if (buttons) {
okCancel.visible = true;

View File

@ -768,26 +768,17 @@ callbacks["MoveFocus"] = (data) => {
cancelAllFocus();
const focuses = data[0];
const command = data[1];
const timeout = data[2] ?? (config.roomTimeout * 1000);
let item, model;
for (let i = 0; i < playerNum; i++) {
model = photoModel.get(i);
if (focuses.indexOf(model.id) != -1) {
item = photos.itemAt(i);
item.progressBar.duration = timeout;
item.progressBar.visible = true;
item.progressTip = luatr(command)
+ luatr(" thinking...");
/*
if (command === "PlayCard") {
item.playing = true;
}
} else {
item = photos.itemAt(i);
if (command === "PlayCard") {
item.playing = false;
}
*/
}
}
}
@ -1241,10 +1232,6 @@ callbacks["AskForResponseCard"] = (data) => {
roomScene.okCancel.visible = true;
}
callbacks["WaitForNullification"] = () => {
roomScene.state = "notactive";
}
callbacks["SetPlayerMark"] = (data) => {
const player = getPhoto(data[0]);
const mark = data[1];
@ -1469,6 +1456,10 @@ callbacks["UpdateMiniGame"] = (data) => {
}
}
callbacks["EmptyRequest"] = (data) => {
roomScene.activate();
}
callbacks["UpdateLimitSkill"] = (data) => {
const id = data[0];
const skill = data[1];

View File

@ -162,6 +162,13 @@ Item {
break;
}
}
})
});
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
if (!card.selectable) {
const reason = lcall("GetCardProhibitReason", card.cid);
card.prohibitReason = reason;
}
}
}
}

View File

@ -627,13 +627,14 @@ Item {
anchors.bottomMargin: -4
from: 0.0
to: 100.0
property int duration: config.roomTimeout * 1000
visible: false
NumberAnimation on value {
running: progressBar.visible
from: 100.0
to: 0.0
duration: config.roomTimeout * 1000
duration: progressBar.duration
onFinished: {
progressBar.visible = false;
@ -665,6 +666,59 @@ Item {
visible: root.state === "candidate" && selectable
}
RowLayout {
anchors.centerIn: parent
spacing: 5
Repeater {
model: root.targetTip
Item {
// Layout.alignment: Qt.AlignHCenter
width: modelData.type === "normal" ? 40 : 24
GlowText {
anchors.centerIn: parent
visible: modelData.type === "normal"
text: Util.processPrompt(modelData.content)
font.family: fontLi2.name
color: "#FEFE84"
font.pixelSize: {
if (text.length <= 3) return 36;
else return 28;
}
//font.bold: true
glow.color: "black"
glow.spread: 0.3
glow.radius: 5
lineHeight: 0.85
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAnywhere
width: font.pixelSize + 4
}
Text {
anchors.centerIn: parent
visible: modelData.type === "warning"
font.family: fontLibian.name
font.pixelSize: 24
opacity: 0.9
horizontalAlignment: Text.AlignHCenter
lineHeight: 24
lineHeightMode: Text.FixedHeight
//color: "#EAC28A"
color: "snow"
width: 24
wrapMode: Text.WrapAnywhere
style: Text.Outline
//styleColor: "#83231F"
styleColor: "red"
text: Util.processPrompt(modelData.content)
}
}
}
}
InvisibleCardArea {
id: handcardAreaItem
anchors.centerIn: parent
@ -739,53 +793,6 @@ Item {
}
}
RowLayout {
anchors.centerIn: parent
spacing: 5
Repeater {
model: root.targetTip
Item {
Layout.alignment: Qt.AlignHCenter
width: 30
GlowText {
anchors.centerIn: parent
visible: modelData.type === "normal"
text: Util.processPrompt(modelData.content)
font.family: fontLibian.name
color: "#F7F589"
font.pixelSize: 30
font.bold: true
glow.color: "black"
glow.spread: 0.3
glow.radius: 5
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAnywhere
width: 30
}
Text {
anchors.centerIn: parent
visible: modelData.type === "warning"
font.family: fontLibian.name
font.pixelSize: 24
opacity: 0.9
horizontalAlignment: Text.AlignHCenter
lineHeight: 24
lineHeightMode: Text.FixedHeight
color: "#EAC28A"
width: 24
wrapMode: Text.WrapAnywhere
style: Text.Outline
styleColor: "#83231F"
text: Util.processPrompt(modelData.content)
}
}
}
}
Rectangle {
color: "#CC2E2C27"
radius: 6

View File

@ -3,8 +3,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.notify.FreeKill"
android:installLocation="preferExternal"
android:versionCode="422"
android:versionName="0.4.22">
android:versionCode="423"
android:versionName="0.4.23">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

@ -213,18 +213,21 @@ fk.client_callback["SetCardFootnote"] = function(data)
ClientInstance:setCardNote(data[1], data[2]);
end
local function setup(id, name, avatar)
local function setup(id, name, avatar, msec)
local self = fk.Self
self:setId(id)
self:setScreenName(name)
self:setAvatar(avatar)
Self = ClientPlayer:new(fk.Self)
if msec then
fk.ClientInstance:setupServerLag(msec)
end
end
fk.client_callback["Setup"] = function(data)
-- jsonData: [ int id, string screenName, string avatar ]
local id, name, avatar = data[1], data[2], data[3]
setup(id, name, avatar)
local id, name, avatar, msec = data[1], data[2], data[3], data[4]
setup(id, name, avatar, msec)
end
fk.client_callback["EnterRoom"] = function(_data)
@ -243,7 +246,7 @@ fk.client_callback["EnterRoom"] = function(_data)
local data = _data[3]
ClientInstance.enter_room_data = json.encode(_data);
ClientInstance.room_settings = data
ClientInstance.settings = data
table.insertTableIfNeed(
data.disabledPack,
Fk.game_mode_disabled[data.gameMode]
@ -857,13 +860,9 @@ fk.client_callback["AskForUseActiveSkill"] = function(data)
local skill = Fk.skills[data[1]]
local extra_data = data[4]
skill._extra_data = extra_data
Fk.currentResponseReason = extra_data.skillName
local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self)
h.skill_name = data[1]
h.prompt = data[2]
h.cancelable = data[3]
h.extra_data = data[4]
local h = Fk.request_handlers["AskForUseActiveSkill"]:new(Self, data)
h.change = {}
h:setup()
h.scene:notifyUI()
@ -873,12 +872,7 @@ end
fk.client_callback["AskForUseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {}
Fk.currentResponsePattern = data[2]
local h = Fk.request_handlers["AskForUseCard"]:new(Self)
-- h.skill_name = data[1] (skill_name是给选中的视为技用的)
h.pattern = data[2]
h.prompt = data[3]
h.cancelable = data[4]
h.extra_data = data[5]
local h = Fk.request_handlers["AskForUseCard"]:new(Self, data)
h.change = {}
h:setup()
h.scene:notifyUI()
@ -888,12 +882,7 @@ end
fk.client_callback["AskForResponseCard"] = function(data)
-- jsonData: card, pattern, prompt, cancelable, {}
Fk.currentResponsePattern = data[2]
local h = Fk.request_handlers["AskForResponseCard"]:new(Self)
-- h.skill_name = data[1] (skill_name是给选中的视为技用的)
h.pattern = data[2]
h.prompt = data[3]
h.cancelable = data[4]
h.extra_data = data[5]
local h = Fk.request_handlers["AskForResponseCard"]:new(Self, data)
h.change = {}
h:setup()
h.scene:notifyUI()
@ -1100,7 +1089,7 @@ fk.client_callback["GameOver"] = function(jsonData)
c.record[2] = table.concat({
c.record[2],
Self.player:getScreenName(),
c.room_settings.gameMode,
c.settings.gameMode,
Self.general,
Self.role,
jsonData,
@ -1118,7 +1107,7 @@ fk.client_callback["EnterLobby"] = function(jsonData)
c.record[2] = table.concat({
c.record[2],
Self.player:getScreenName(),
c.room_settings.gameMode,
c.settings.gameMode,
Self.general,
Self.role,
"",

View File

@ -335,49 +335,6 @@ function CanUseCardToTarget(card, to_select, selected, extra_data_str)
return ret
end
---@param card string | integer
---@param to_select integer @ id of the target
---@param selected integer[] @ ids of selected targets
---@param selectable bool
---@param extra_data_str string @ extra data
function GetUseCardTargetTip(card, to_select, selected, selectable, extra_data_str)
local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str)
local c ---@type Card
local selected_cards
if type(card) == "number" then
c = Fk:getCardById(card)
selected_cards = {card}
else
local t = json.decode(card)
return ActiveTargetTip(t.skill, to_select, selected, t.subcards, selectable, extra_data)
end
local ret
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
ret = ret or {}
if #ret > 4 then
return ret
end
local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, c, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
ret = ret or {}
local tip = c.skill:targetTip(to_select, selected, selected_cards, c, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
return ret
end
---@param card string | integer
---@param to_select integer @ id of a card not selected
---@param selected_targets integer[] @ ids of selected players
@ -457,10 +414,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
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
@ -543,46 +515,6 @@ function ActiveTargetFilter(skill_name, to_select, selected, selected_cards, ext
return ret
end
function ActiveTargetTip(skill_name, to_select, selected, selected_cards, selectable, extra_data)
local skill = Fk.skills[skill_name]
local ret
if skill then
if skill:isInstanceOf(ActiveSkill) then
ret = skill:targetTip(to_select, selected, selected_cards, nil, selectable)
if type(ret) == "string" then
ret = { { content = ret, type = "normal" } }
end
elseif skill:isInstanceOf(ViewAsSkill) then
local card = skill:viewAs(selected_cards)
if card then
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
ret = ret or {}
if #ret > 4 then
return ret
end
local tip = skill:getTargetTip(Self, to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
ret = ret or {}
local tip = card.skill:targetTip(to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = ret, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
end
end
return ret
end
function ActiveFeasible(skill_name, selected, selected_cards)
local skill = Fk.skills[skill_name]
local ret = false
@ -746,7 +678,7 @@ end
function ResetClientLua()
local _data = ClientInstance.enter_room_data;
local data = ClientInstance.room_settings
local data = ClientInstance.settings
Self = ClientPlayer:new(fk.Self)
ClientInstance = Client:new() -- clear old client data
ClientInstance.players = {Self}
@ -754,7 +686,7 @@ function ResetClientLua()
ClientInstance.discard_pile = {}
ClientInstance.enter_room_data = _data;
ClientInstance.room_settings = data
ClientInstance.settings = data
ClientInstance.disabled_packs = data.disabledPack
ClientInstance.disabled_generals = data.disabledGenerals
@ -766,7 +698,7 @@ function ResetAddPlayer(j)
end
function GetRoomConfig()
return ClientInstance.room_settings
return ClientInstance.settings
end
function GetPlayerGameData(pid)
@ -813,7 +745,7 @@ function SetReplayingShowCards(o)
end
function CheckSurrenderAvailable(playedTime)
local curMode = ClientInstance.room_settings.gameMode
local curMode = ClientInstance.settings.gameMode
return Fk.game_modes[curMode]:surrenderFunc(playedTime)
end
@ -822,17 +754,38 @@ function SaveRecord()
c.client:saveRecord(json.encode(c.record), c.record[2])
end
function GetCardProhibitReason(cid, method, pattern)
function GetCardProhibitReason(cid)
local card = Fk:getCardById(cid)
if not card then return "" end
local handler = ClientInstance.current_request_handler
if (not handler) or (not handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then return "" end
local method, pattern = "", handler.pattern or "."
if handler.class.name == "ReqPlayCard" then method = "play"
elseif handler.class.name == "ReqResponseCard" then method = "response"
elseif handler.class.name == "ReqUseCard" then method = "use"
elseif handler.skill_name == "discard_skill" then method = "discard"
end
if method == "play" and not card.skill:canUse(Self, card) then return "" end
if method ~= "play" and not card:matchPattern(pattern) then return "" end
if method == "play" then method = "use" end
local fn_table = {
use = "prohibitUse",
response = "prohibitResponse",
discard = "prohibitDiscard",
}
local str_table = {
use = "method_use",
response = "method_response_play",
discard = "method_discard",
}
local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable
local s
for _, skill in ipairs(status_skills) do
local fn = method == "use" and skill.prohibitUse or skill.prohibitResponse
local fn = skill[fn_table[method]]
if fn(skill, Self, card) then
s = skill
break
@ -844,14 +797,70 @@ function GetCardProhibitReason(cid, method, pattern)
local skillName = s.name
local ret = Fk:translate(skillName)
if ret ~= skillName then
return ret .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play")
return ret .. Fk:translate("prohibit") .. Fk:translate(str_table[method])
elseif skillName:endsWith("_prohibit") and skillName:startsWith("#") then
return Fk:translate(skillName:sub(2, -10)) .. Fk:translate("prohibit") .. Fk:translate(method == "use" and "method_use" or "method_response_play")
return Fk:translate(skillName:sub(2, -10)) .. Fk:translate("prohibit") .. Fk:translate(str_table[method])
else
return ret
end
end
function GetTargetTip(pid)
local handler = ClientInstance.current_request_handler --[[@as ReqPlayCard ]]
if (not handler) or (not handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then return "" end
local to_select = pid
local selected = handler.selected_targets
local selected_cards = handler.pendings
local card = handler.selected_card --[[@as Card?]]
local skill = Fk.skills[handler.skill_name]
local photo = handler.scene.items["Photo"][pid] --[[@as Photo]]
local selectable = photo.enabled
local extra_data = handler.extra_data
local ret = {}
if skill then
if skill:isInstanceOf(ActiveSkill) then
local tip = skill:targetTip(to_select, selected, selected_cards, nil, selectable)
if type(tip) == "string" then
table.insert(ret, { content = tip, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
elseif skill:isInstanceOf(ViewAsSkill) then
card = skill:viewAs(selected_cards)
end
end
if card then
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable
for _, sk in ipairs(status_skills) do
ret = ret or {}
if #ret > 4 then
return ret
end
local tip = sk:getTargetTip(Self, to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = tip, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
ret = ret or {}
local tip = card.skill:targetTip(to_select, selected, selected_cards, card, selectable, extra_data)
if type(tip) == "string" then
table.insert(ret, { content = tip, type = "normal" })
elseif type(tip) == "table" then
table.insertTable(ret, tip)
end
end
return ret
end
function CanSortHandcards(pid)
return ClientInstance:getPlayerById(pid):getMark(MarkEnum.SortProhibited) == 0
end
@ -940,7 +949,10 @@ function RevertSelection()
h.scene:notifyUI()
end
local requestUIUpdating = false
function UpdateRequestUI(elemType, id, action, data)
if requestUIUpdating then return end
requestUIUpdating = true
local h = ClientInstance.current_request_handler
h.change = {}
local finish = h:update(elemType, id, action, data)
@ -949,12 +961,14 @@ function UpdateRequestUI(elemType, id, action, data)
else
h:_finish()
end
requestUIUpdating = false
end
function FinishRequestUI()
local h = ClientInstance.current_request_handler
if h then
h:_finish()
ClientInstance.current_request_handler = nil
end
end

View File

@ -236,7 +236,7 @@ Fk:loadTranslationTable({
["#replaceEquip"] = "Please Choose a Equip Card to be replaced",
["#askForPindian"] = "%arg: please choose a hand card for point fight",
["#StartPindianReason"] = "%from started point fight (%arg)",
["#ShowPindianCard"] = "The point fight card of %from is %card",
["#ShowPindianCard"] = "The point fight card of %from is %arg",
["#ShowPindianResult"] = "%from %arg the point fight between %from and %to",
["pindianwin"] = "won",
["pindiannotwin"] = "lost",

View File

@ -205,6 +205,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["Cancel"] = "取消",
["End"] = "结束",
-- ["Quit"] = "退出",
["All"] = "全部",
["BanGeneral"] = "禁将",
["ResumeGeneral"] = "解禁",
["Enable"] = "启用",
@ -293,7 +294,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["#replaceEquip"] = "选择一张装备牌替换之",
["#askForPindian"] = "%arg请选择一张手牌作为拼点牌",
["#StartPindianReason"] = "%from 由于 %arg 而发起拼点",
["#ShowPindianCard"] = "%from 的拼点牌是 %card",
["#ShowPindianCard"] = "%from 的拼点牌是 %arg",
["#ShowPindianResult"] = "%from 在 %from 和 %to 之间的拼点中 %arg",
["pindianwin"] = "",
["pindiannotwin"] = "没赢",

View File

@ -23,7 +23,7 @@ function Traceback()
end
local msgh = function(err)
fk.qCritical(err .. "\n" .. debug.traceback(nil, 2))
fk.qCritical(tostring(err) .. "\n" .. debug.traceback(nil, 2))
end
function Pcall(f, ...)
@ -36,3 +36,10 @@ end
function p(v) print(inspect(v)) end
function pt(t) for k, v in pairs(t) do print(k, v) end end
local _verbose = false
function verbose(fmt, ...)
if not _verbose then return end
local str = fmt:format(...)
fk.qInfo(str)
end

View File

@ -19,10 +19,12 @@
---@field public same_generals table<string, string[]> @ 所有同名武将组合
---@field public lords string[] @ 所有主公武将,用于常备主公
---@field public all_card_types table<string, Card> @ 所有的卡牌类型以及一张样板牌
---@field public all_card_names string[] @ 有序的所有的卡牌牌名,顺序:基本牌(杀置顶),普通锦囊,延时锦囊,按副类别排序的装备
---@field public cards Card[] @ 所有卡牌
---@field public translations table<string, table<string, string>> @ 翻译表
---@field public game_modes table<string, GameMode> @ 所有游戏模式
---@field public game_mode_disabled table<string, string[]> @ 游戏模式禁用的包
---@field public main_mode_list table<string, string[]> @ 主模式检索表
---@field public currentResponsePattern string @ 要求用牌的种类(如要求用特定花色的桃···)
---@field public currentResponseReason string @ 要求用牌的原因(如濒死,被特定牌指定,使用特定技能···)
---@field public filtered_cards table<integer, Card> @ 被锁视技影响的卡牌
@ -66,10 +68,12 @@ function Engine:initialize()
self.same_generals = {}
self.lords = {} -- lordName[]
self.all_card_types = {}
self.all_card_names = {}
self.cards = {} -- Card[]
self.translations = {} -- srcText --> translated
self.game_modes = {}
self.game_mode_disabled = {}
self.main_mode_list = {}
self.kingdoms = {}
self.kingdom_map = {}
self.damage_nature = { [fk.NormalDamage] = { "normal_damage", false } }
@ -82,6 +86,7 @@ function Engine:initialize()
self:loadPackages()
self:setLords()
self:loadCardNames()
self:loadDisabled()
self:loadRequestHandlers()
self:addSkills(AuxSkills)
@ -265,9 +270,28 @@ function Engine:loadDisabled()
for mode_name, game_mode in pairs(self.game_modes) do
local disabled_packages = {}
for name, pkg in pairs(self.packages) do
if table.contains(game_mode.blacklist or Util.DummyTable, name) or
(game_mode.whitelist and not table.contains(game_mode.whitelist, name)) or
table.contains(pkg.game_modes_blacklist or Util.DummyTable, mode_name) or
--- GameMode对Package筛选
if type(game_mode.whitelist) == "function" then
if not game_mode:whitelist(pkg) then
table.insert(disabled_packages, name)
end
elseif type(game_mode.whitelist) == "table" then
if not table.contains(game_mode.whitelist, name) then
table.insert(disabled_packages, name)
end
end
if type(game_mode.blacklist) == "function" then
if game_mode:blacklist(pkg) then
table.insert(disabled_packages, name)
end
elseif type(game_mode.blacklist) == "table" then
if table.contains(game_mode.blacklist, name) then
table.insert(disabled_packages, name)
end
end
--- Package对GameMode筛选
if table.contains(pkg.game_modes_blacklist or Util.DummyTable, mode_name) or
(pkg.game_modes_whitelist and not table.contains(pkg.game_modes_whitelist, mode_name)) then
table.insert(disabled_packages, name)
end
@ -387,6 +411,7 @@ function Engine:setLords()
for _, skill in ipairs(skills) do
if skill.lordSkill then
table.insert(self.lords, general.name)
break
end
end
end
@ -498,6 +523,7 @@ function Engine:addCard(card)
if self.all_card_types[card.name] == nil then
self.skills[card.skill.name] = card.skill
self.all_card_types[card.name] = card
table.insert(self.all_card_names, card.name)
end
end
@ -524,6 +550,23 @@ function Engine:cloneCard(name, suit, number)
return ret
end
--- 为所有加载的卡牌牌名排序
function Engine:loadCardNames()
local slash, basic, commonTrick, other = {}, {}, {}, {}
for _, name in ipairs(self.all_card_names) do
local card = self.all_card_types[name]
if card.type == Card.TypeBasic then
table.insert(card.trueName == "slash" and slash or basic, name)
elseif card:isCommonTrick() then
table.insert(commonTrick, name)
else
table.insert(other, name)
end
end
table.sort(other, function(a, b) return self.all_card_types[a].sub_type < self.all_card_types[b].sub_type end)
self.all_card_names = table.connect(slash, basic, commonTrick, other)
end
--- 向Engine中添加一系列游戏模式。
---@param game_modes GameMode[] @ 要添加的游戏模式列表
function Engine:addGameModes(game_modes)

View File

@ -1,6 +1,22 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
-- 呃 起码要做出以下几种吧:
--- GameMode用来描述一个游戏模式。
---
--- 可以参考欢乐斗地主。
---
---@class GameMode: Object
---@field public name string @ 游戏模式名
---@field public minPlayer integer @ 最小玩家数
---@field public maxPlayer integer @ 最大玩家数
---@field public rule? TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件)
---@field public logic? fun(): GameLogic @ 逻辑通过function完成通常用来初始化、分配身份及座次
---@field public whitelist? string[] | fun(self: GameMode, pkg: Package): bool @ 白名单
---@field public blacklist? string[] | fun(self: GameMode, pkg: Package): bool @ 黑名单
---@field public config_template? GameModeConfigEntry[] 游戏模式的配置页面,如此一个数组
---@field public main_mode? string @ 主模式名(用于判断此模式是否为某模式的衍生)
local GameMode = class("GameMode")
-- 呃 起码要做出以下几种吧:(class必须放顶层否则文档那个东西不识别 辣鸡
-- Switch那个开关组件 [boolean model=nil]
-- RadioButton那个多选一圆圈里面打点组件 [string model={ label, value }[]]
-- ComboBox: 那个多选一,下拉一个菜单并选择一个的组件 [string model同上]
@ -16,21 +32,6 @@
---@field public model any @ 这种delegate需要的model参见注释
---@field public default? any @ 默认值 cfg的value
--- GameMode用来描述一个游戏模式。
---
--- 可以参考欢乐斗地主。
---
---@class GameMode: Object
---@field public name string @ 游戏模式名
---@field public minPlayer integer @ 最小玩家数
---@field public maxPlayer integer @ 最大玩家数
---@field public rule? TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件)
---@field public logic? fun(): GameLogic @ 逻辑通过function完成通常用来初始化、分配身份及座次
---@field public whitelist? string[] @ 白名单
---@field public blacklist? string[] @ 黑名单
---@field public config_template? GameModeConfigEntry[] 游戏模式的配置页面,如此一个数组
local GameMode = class("GameMode")
--- 构造函数,不可随意调用。
---@param name string @ 游戏模式名
---@param min integer @ 最小玩家数
@ -100,14 +101,17 @@ function GameMode:getAdjustedProperty (player)
return list
end
--- 向游戏模式中添加拓展包过滤。
---@param whitelist string[] @ 白名单
---@param blacklist string[] @ 黑名单
function GameMode:addPackageFilter(whitelist, blacklist)
assert(type(whitelist) == "table")
assert(type(blacklist) == "table")
table.insertTable(self.whitelist, whitelist)
table.insertTable(self.blacklist, blacklist)
-- 执行死亡奖惩
---@param victim ServerPlayer @ 死亡角色
---@param killer? ServerPlayer @ 击杀者
function GameMode:deathRewardAndPunish (victim, killer)
if not killer or killer.dead then return end
if victim.role == "rebel" then
killer:drawCards(3, "kill")
elseif victim.role == "loyalist" and killer.role == "lord" then
killer:throwAllCards("he")
end
end
return GameMode

View File

@ -85,21 +85,4 @@ function Package:addGameMode(game_mode)
table.insert(self.game_modes, game_mode)
end
--- 向拓展包中设置游戏模式过滤。
---@param whitelist string[] @ 白名单
---@param blacklist string[] @ 黑名单
function Package:setGameModeFilter(whitelist, blacklist)
self.game_modes_whitelist = whitelist
self.game_modes_blacklist = blacklist
end
--- 向拓展包中添加游戏模式过滤。
---@param whitelist string[] @ 白名单
---@param blacklist string[] @ 黑名单
function Package:addGameModeFilter(whitelist, blacklist)
assert(type(whitelist) == "table")
assert(type(blacklist) == "table")
table.insertTable(self.game_modes_whitelist, whitelist)
table.insertTable(self.game_modes_blacklist, blacklist)
end
return Package

View File

@ -1194,6 +1194,25 @@ function Player:isBuddy(other)
return self.id == id or table.contains(self.buddy_list, id)
end
local function defaultCardVisible(self, cardId, area, owner, falsy)
local public_areas = {Card.DiscardPile, Card.Processing, Card.Void, Card.PlayerEquip, Card.PlayerJudge}
local player_areas = {Card.PlayerHand, Card.PlayerSpecial}
if area == Card.DrawPile then return false
elseif table.contains(public_areas, area) then return not falsy
elseif table.contains(player_areas, area) then
if area == Card.PlayerSpecial then
local specialName = owner:getPileNameOfId(cardId)
if not specialName:startsWith("$") then
return true
end
end
return owner == self or self:isBuddy(owner)
else
return false
end
end
--- Player是否可看到某card
--- @param cardId integer
---@param move? CardsMoveStruct
@ -1203,11 +1222,15 @@ function Player:cardVisible(cardId, move)
if room.replaying and room.replaying_show then return true end
local falsy = false -- 当难以决定时是否要选择暗置?
local oldarea, oldowner
if move then
if table.find(move.moveInfo, function(info) return info.cardId == cardId end) then
---@type MoveInfo
local info = table.find(move.moveInfo, function(info) return info.cardId == cardId end)
if info then
oldarea = info.fromArea
oldowner = move.from and room:getPlayerById(move.from)
if move.moveVisible then return true end
if move.moveVisible == false then falsy = true end
-- specialVisible还要控制这个pile对他人是否应该可见但是不在这里生效
if move.specialVisible then return true end
if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or
@ -1218,11 +1241,9 @@ function Player:cardVisible(cardId, move)
end
local area = room:getCardArea(cardId)
local owner = room:getCardOwner(cardId)
local card = Fk:getCardById(cardId)
local public_areas = {Card.DiscardPile, Card.Processing, Card.Void, Card.PlayerEquip, Card.PlayerJudge}
local player_areas = {Card.PlayerHand, Card.PlayerSpecial}
if room.observing and not room.replaying then return table.contains(public_areas, area) end
local status_skills = Fk:currentRoom().status_skills[VisibilitySkill] or Util.DummyTable
@ -1233,16 +1254,14 @@ function Player:cardVisible(cardId, move)
end
end
if area == Card.DrawPile then return false
elseif table.contains(public_areas, area) then return not falsy
elseif move and area == Card.PlayerSpecial and not move.specialName:startsWith("$") then
return not falsy
elseif table.contains(player_areas, area) then
local to = room:getCardOwner(cardId)
return to == self or self:isBuddy(to)
else
return false
if defaultCardVisible(self, cardId, area, owner, falsy) then
return true
elseif oldarea then
-- 尽可能让牌可见
return defaultCardVisible(self, cardId, oldarea, oldowner, falsy)
end
return false
end
--- Player是否可看到某target的身份
@ -1328,7 +1347,7 @@ function Player:loadJsonObject(o)
for _, id in ipairs(o.player_cards[Player.Judge]) do
room:setCardArea(id, Card.PlayerJudge, pid)
end
for _, ids in ipairs(o.special_cards) do
for _, ids in pairs(o.special_cards) do
for _, id in ipairs(ids) do
room:setCardArea(id, Card.PlayerSpecial, pid)
end

View File

@ -37,7 +37,7 @@
---@field public change { [string]: Item[] } 将会传递给UI的更新数据
local RequestHandler = class("RequestHandler")
function RequestHandler:initialize(player)
function RequestHandler:initialize(player, data)
self.room = Fk:currentRoom()
self.player = player
-- finish只在Client执行 用于保证UI执行了某些必须执行的善后

View File

@ -25,11 +25,18 @@ local CardItem = (require 'ui_emu.common').CardItem
---@field public expanded_piles { [string]: integer[] } 用于展开/收起
local ReqActiveSkill = RequestHandler:subclass("ReqActiveSkill")
function ReqActiveSkill:initialize(player)
function ReqActiveSkill:initialize(player, data)
RequestHandler.initialize(self, player)
self.scene = RoomScene:new(self)
self.expanded_piles = {}
if data then
self.skill_name = data[1]
self.prompt = data[2]
self.cancelable = data[3]
self.extra_data = data[4]
end
end
function ReqActiveSkill:setup(ignoreInteraction)
@ -55,6 +62,7 @@ function ReqActiveSkill:setup(ignoreInteraction)
self:updateUnselectedTargets()
self:updateButtons()
self:updatePrompt()
end
function ReqActiveSkill:finish()
@ -73,6 +81,15 @@ function ReqActiveSkill:setSkillPrompt(skill, cid)
end
end
function ReqActiveSkill:updatePrompt()
local skill = Fk.skills[self.skill_name]
if skill then
self:setSkillPrompt(skill)
else
self:setPrompt(self.original_prompt or "")
end
end
function ReqActiveSkill:setupInteraction()
local skill = Fk.skills[self.skill_name]
if skill and skill.interaction then
@ -80,7 +97,7 @@ function ReqActiveSkill:setupInteraction()
if not interaction then
return
end
skill.interaction.data = interaction.default_choice or nil -- FIXME
skill.interaction.data = interaction.default or interaction.default_choice or nil -- FIXME
-- 假设只有1个interaction (其实目前就是这样)
local i = Interaction:new(self.scene, "1", interaction)
i.skill_name = self.skill_name
@ -291,11 +308,19 @@ function ReqActiveSkill:doOKButton()
if self.selected_card then
reply.special_skill = self.skill_name
end
if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
else
return reply
end
end
function ReqActiveSkill:doCancelButton()
if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", "__cancel")
else
return "__cancel"
end
end
-- 对点击卡牌的处理。data中包含selected属性可能是选中或者取消选中分开考虑。
@ -365,6 +390,7 @@ function ReqActiveSkill:update(elemType, id, action, data)
elseif elemType == "Interaction" then
self:updateInteraction(data)
end
self:updatePrompt()
end
return ReqActiveSkill

View File

@ -19,7 +19,6 @@ end
function ReqPlayCard:setup()
ReqUseCard.setup(self)
self:setPrompt(self.original_prompt)
self.scene:update("Button", "End", { enabled = true })
end
@ -85,14 +84,22 @@ end
function ReqPlayCard:feasible()
local player = self.player
if self.skill_name then
return ReqActiveSkill.feasible(self)
end
local card = self.selected_card
local ret = false
local card = self.selected_card
if self.skill_name then
local skill = Fk.skills[self.skill_name]
if skill:isInstanceOf(ActiveSkill) then
return ReqActiveSkill.feasible(self)
else -- viewasskill
card = skill:viewAs(self.pendings)
end
end
if card then
local skill = card.skill ---@type ActiveSkill
ret = skill:feasible(self.selected_targets, { card.id }, player, card)
if ret then
ret = skill:canUse(player, card, self.extra_data) and not player:prohibitUse(card)
end
end
return ret
end
@ -107,11 +114,11 @@ function ReqPlayCard:selectSpecialUse(data)
if not data or data == "_normal_use" then
self.skill_name = nil
self.pendings = nil
self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId())
-- self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId())
else
self.skill_name = data
self.pendings = Card:getIdList(self.selected_card)
self:setSkillPrompt(Fk.skills[data], self.pendings)
-- self:setSkillPrompt(Fk.skills[data], self.pendings)
end
self:initiateTargets()
end
@ -131,7 +138,11 @@ end
function ReqPlayCard:doEndButton()
self.scene:update("SpecialSkills", "1", { skills = {} })
self.scene:notifyUI()
if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", "")
else
return ""
end
end
function ReqPlayCard:selectCard(cid, data)
@ -146,7 +157,7 @@ function ReqPlayCard:selectCard(cid, data)
self.skill_name = nil
self.selected_card = Fk:getCardById(cid)
scene:unselectOtherCards(cid)
self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId())
-- self:setSkillPrompt(self.selected_card.skill, self.selected_card:getEffectiveId())
local sp_skills = {}
if self.selected_card.special_skills then
sp_skills = table.simpleClone(self.selected_card.special_skills)

View File

@ -17,6 +17,19 @@ local ReqActiveSkill = require 'core.request_type.active_skill'
---@field public original_prompt string 最开始的提示信息;这种涉及技能按钮的需要这样一下
local ReqResponseCard = ReqActiveSkill:subclass("ReqResponseCard")
function ReqResponseCard:initialize(player, data)
ReqActiveSkill.initialize(self, player)
if data then
-- self.skill_name = data[1] (skill_name是给选中的视为技用的)
self.pattern = data[2]
self.prompt = data[3]
self.cancelable = data[4]
self.extra_data = data[5]
self.disabledSkillNames = data[6]
end
end
function ReqResponseCard:setup()
if not self.original_prompt then
self.original_prompt = self.prompt or ""
@ -25,6 +38,7 @@ function ReqResponseCard:setup()
ReqActiveSkill.setup(self)
self.selected_card = nil
self:updateSkillButtons()
self:updatePrompt()
end
-- FIXME: 关于&牌堆的可使用打出瞎jb写了点 来个懂哥优化一下
@ -41,8 +55,12 @@ end
function ReqResponseCard:skillButtonValidity(name)
local player = self.player
local skill = Fk.skills[name]
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, true)
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern)
return
skill:isInstanceOf(ViewAsSkill) and
skill:enabledAtResponse(player, true) and
skill.pattern and
Exppattern:Parse(self.pattern):matchExp(skill.pattern) and
not table.contains(self.disabledSkillNames or {}, name)
end
function ReqResponseCard:cardValidity(cid)
@ -86,7 +104,11 @@ function ReqResponseCard:doOKButton()
card = self.selected_card:getEffectiveId(),
targets = self.selected_targets,
}
if ClientInstance then
ClientInstance:notifyUI("ReplyToServer", json.encode(reply))
else
return reply
end
end
function ReqResponseCard:doCancelButton()
@ -109,9 +131,10 @@ function ReqResponseCard:selectSkill(skill, data)
end
self.skill_name = skill
self.selected_card = nil
self:setSkillPrompt(skill)
ReqActiveSkill.setup(self)
-- self:setSkillPrompt(Fk.skills[skill])
else
self.skill_name = nil
self.prompt = self.original_prompt

View File

@ -4,11 +4,27 @@ local ReqResponseCard = require 'core.request_type.response_card'
---@class ReqUseCard: ReqResponseCard
local ReqUseCard = ReqResponseCard:subclass("ReqUseCard")
function ReqUseCard:updatePrompt()
if self.skill_name then
return ReqActiveSkill.updatePrompt(self)
end
local card = self.selected_card
if card and card.skill then
self:setSkillPrompt(card.skill, self.selected_card.id)
else
self:setPrompt(self.original_prompt or "")
end
end
function ReqUseCard:skillButtonValidity(name)
local player = self.player
local skill = Fk.skills[name]
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, false)
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern)
return
skill:isInstanceOf(ViewAsSkill) and
skill:enabledAtResponse(player, false) and
skill.pattern and
Exppattern:Parse(self.pattern):matchExp(skill.pattern) and
not table.contains(self.disabledSkillNames or {}, name)
end
function ReqUseCard:cardValidity(cid)
@ -45,7 +61,7 @@ function ReqUseCard:targetValidity(pid)
-- 若include中全都没选且target不在include中则不可选择
if table.every(data.include_targets, function(id)
return not table.contains(self.selected_targets, id)
end) and not table.contains(data.must_targets, pid) then
end) and not table.contains(data.include_targets, pid) then
ret = false
end
end
@ -108,7 +124,7 @@ function ReqUseCard:selectTarget(playerid, data)
self.selected_targets = {}
for _, pid in ipairs(previous_targets) do
local ret
ret = not player:isProhibited(pid, card) and skill and
ret = not player:isProhibited(self.room:getPlayerById(pid), card) and skill and
skill:targetFilter(pid, self.selected_targets,
{ card.id }, card, data.extra_data)
-- 从头开始写目标

View File

@ -89,4 +89,11 @@ function AbstractRoom:loadJsonObject(o)
end
end
-- 判断当前模式是否为某类模式
---@param mode string @ 需要判定的模式类型
---@return boolean
function AbstractRoom:isGameMode(mode)
return table.contains(Fk.main_mode_list[mode] or {}, self.settings.gameMode)
end
return AbstractRoom

View File

@ -156,4 +156,16 @@ function Skill:getTimes()
return ret
end
-- 获得此技能时,触发此函数
---@param player ServerPlayer
---@param is_start bool
function Skill:onAcquire(player, is_start)
end
-- 失去此技能时,触发此函数
---@param player ServerPlayer
---@param is_death bool
function Skill:onLose(player, is_death)
end
return Skill

View File

@ -57,15 +57,18 @@ end
-- DO NOT modify this function
function TriggerSkill:doCost(event, target, player, data)
local start_time = os.getms()
local room = player.room
room.current_cost_skill = self
local ret = self:cost(event, target, player, data)
local end_time = os.getms()
local room = player.room
-- 对于那种cost直接返回true的锁定技如果是预亮技那么还是询问一下好
if ret and player:isFakeSkill(self) and end_time - start_time < 10000 and
if ret and player:isFakeSkill(self) and end_time - start_time < 1000 and
(self.main_skill and self.main_skill or self).visible then
ret = room:askForSkillInvoke(player, self.name)
end
room.current_cost_skill = nil
local cost_data_bak = self.cost_data
room.logic:trigger(fk.BeforeTriggerSkillUse, player, { skill = self, willUse = ret })

View File

@ -92,4 +92,25 @@ function UsableSkill:withinTimesLimit(player, scope, card, card_name, to)
-- end)))
end
-- 失去此技能时,触发此函数
---@param player ServerPlayer
---@param is_death bool
function UsableSkill:onLose(player, is_death)
local lost_piles = {}
if self.derived_piles then
for _, pile_name in ipairs(self.derived_piles) do
table.insertTableIfNeed(lost_piles, player:getPile(pile_name))
end
end
if #lost_piles > 0 then
player.room:moveCards({
ids = lost_piles,
from = player.id,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
end
end
return UsableSkill

View File

@ -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
@ -729,4 +730,15 @@ function AimGroup:getCancelledTargets(aimGroup)
return aimGroup[AimGroup.Cancelled]
end
---@param target ServerPlayer
---@param data AimStruct
---@return boolean
function AimGroup:isOnlyTarget(target, data)
if data.tos == nil then return false end
local tos = AimGroup:getAllTargets(data.tos)
return table.contains(tos, target.id) and not table.find(target.room.alive_players, function (p)
return p ~= target and table.contains(tos, p.id)
end)
end
return { TargetGroup, AimGroup, Util }

View File

@ -45,6 +45,16 @@ local function readCommonSpecToSkill(skill, spec)
assert(type(spec.relate_to_place) == "string")
skill.relate_to_place = spec.relate_to_place
end
if spec.on_acquire then
assert(type(spec.on_acquire) == "function")
skill.onAcquire = spec.on_acquire
end
if spec.on_lose then
assert(type(spec.on_lose) == "function")
skill.onLose = spec.on_lose
end
end
local function readUsableSpecToSkill(skill, spec)
@ -88,6 +98,8 @@ end
---@field public max_round_use_time? integer
---@field public max_game_use_time? integer
---@field public times? integer | fun(self: UsableSkill): integer
---@field public on_acquire? fun(self: UsableSkill, player: ServerPlayer, is_start: boolean)
---@field public on_lose? fun(self: UsableSkill, player: ServerPlayer, is_death: boolean)
---@class StatusSkillSpec: StatusSkill
@ -191,6 +203,7 @@ end
---@field public mod_target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], user: integer, card: Card, distance_limited: boolean): boolean?
---@field public prompt? string|fun(self: ActiveSkill, selected_cards: integer[], selected_targets: integer[]): string
---@field public interaction any
---@field public target_tip? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, selectable: boolean, extra_data: any): string|TargetTipDataSpec?
---@param spec ActiveSkillSpec
---@return ActiveSkill
@ -635,7 +648,14 @@ function fk.CreateTreasure(spec)
return card
end
---@param spec GameMode
---@class GameModeSpec: GameMode
---@field public winner_getter? fun(self: GameMode, victim: ServerPlayer): string
---@field public surrender_func? fun(self: GameMode, playedTime: number): string
---@field public is_counted? fun(self: GameMode, room: Room): boolean
---@field public get_adjusted? fun(self: GameMode, player: ServerPlayer): table
---@field public reward_punish? fun(self: GameMode, victim: ServerPlayer, killer: ServerPlayer)
---@param spec GameModeSpec
---@return GameMode
function fk.CreateGameMode(spec)
assert(type(spec.name) == "string")
@ -646,6 +666,9 @@ function fk.CreateGameMode(spec)
ret.blacklist = spec.blacklist
ret.rule = spec.rule
ret.logic = spec.logic
ret.main_mode = spec.main_mode or spec.name
Fk.main_mode_list[ret.main_mode] = Fk.main_mode_list[ret.main_mode] or {}
table.insert(Fk.main_mode_list[ret.main_mode], ret.name)
if spec.winner_getter then
assert(type(spec.winner_getter) == "function")
@ -663,6 +686,10 @@ function fk.CreateGameMode(spec)
assert(type(spec.get_adjusted) == "function")
ret.getAdjustedProperty = spec.get_adjusted
end
if spec.reward_punish then
assert(type(spec.reward_punish) == "function")
ret.deathRewardAndPunish = spec.reward_punish
end
return ret
end

View File

@ -79,17 +79,18 @@ end
Config = loadConf()
-- 禁用各种危险的函数尽可能让Lua执行安全的代码。
local _os = {
os = {
time = os.time,
date = os.date,
clock = os.clock,
difftime = os.difftime,
getms = os.getms,
}
os = _os
io = nil
io = {
lines = io.lines
}
package = nil
load = nil
-- load = nil
loadfile = nil
local _dofile = dofile
dofile = function(f)

View File

@ -245,7 +245,9 @@ local function where(info, context_lines)
local filename = info.source:match("@(.*)")
if filename then
if UsingNewCore and (filename:startsWith("./lua/") or filename:startsWith("lua/")) then
if not FileIO.pwd():endsWith("packages/freekill-core") then
filename = "./packages/freekill-core/" .. filename
end
end
pcall(function() for line in io.lines(filename) do table.insert(source, line) end end)
elseif info.source then

View File

@ -24,7 +24,10 @@ function Object:initialize(...) end
---@return T
function Object:new(...)end
---@generic T
---@param self T
---@param name string
---@return T
function Object:subclass(name)end
---@param class class|Object

View File

@ -7,39 +7,222 @@
---@field public room Room
---@field public player ServerPlayer
---@field public command string
---@field public jsonData string
---@field public cb_table table<string, fun(self: AI, jsonData: string)>
---@field public data any
---@field public handler ReqActiveSkill 可能空,但是没打问号(免得一堆警告)
local AI = class("AI")
function AI:initialize(player)
---@diagnostic disable-next-line
self.room = RoomInstance
self.player = player
local cb_t = {}
-- default strategy: print command and data, then return ""
setmetatable(cb_t, {
__index = function()
return function()
print(self.command, self.jsonData)
return ""
end
end,
})
self.cb_table = cb_t
end
function AI:readRequestData()
self.command = self.player.ai_data.command
self.jsonData = self.player.ai_data.jsonData
function AI:__tostring()
return string.format("%s: %s", self.class.name, tostring(self.player))
end
-- activeSkill, responseCard, useCard, playCard 四巨头专属
function AI:isInDashboard()
if not (self.handler and self.handler:isInstanceOf(Fk.request_handlers["AskForUseActiveSkill"])) then
fk.qWarning("请检查是否在AI中调用了专属于dashboard操作的一系列函数")
fk.qWarning(debug.traceback())
return false
end
return true
end
function AI:getPrompt()
local handler = self.handler
if not handler then return "" end
return handler.prompt
end
--- 返回当前手牌区域内包含展开的pile中所有可选且未选中的卡牌 返回ids
---@param pattern string? 可以带一个过滤条件
---@return integer[]
function AI:getEnabledCards(pattern)
if not self:isInDashboard() then return Util.DummyTable end
local ret = {}
for cid, item in pairs(self.handler.scene:getAllItems("CardItem")) do
if item.enabled and not item.selected then
if (not pattern) or Exppattern:Parse(pattern):match(Fk:getCardById(cid)) then
table.insert(ret, cid)
end
end
end
return ret
end
--- 返回当前所有可选并且还未选中的角色,包括自己
---@return ServerPlayer[]
function AI:getEnabledTargets()
if not self:isInDashboard() then return Util.DummyTable end
local room = self.room
local ret = {}
for pid, item in pairs(self.handler.scene:getAllItems("Photo")) do
if item.enabled and not item.selected then
table.insert(ret, room:getPlayerById(pid))
end
end
return ret
end
function AI:hasEnabledTarget()
if not self:isInDashboard() then return false end
local room = self.room
for _, item in pairs(self.handler.scene:getAllItems("Photo")) do
if item.enabled and not item.selected then
return true
end
end
return false
end
--- 获取技能面板中所有可以按下的技能按钮
---@return string[]
--- 获取技能面板中所有可以按下的技能按钮
---@return string[]
function AI:getEnabledSkills()
if not self:isInDashboard() then return Util.DummyTable end
local ret = {}
for name, item in pairs(self.handler.scene:getAllItems("SkillButton")) do
if item.enabled and not item.selected then
table.insert(ret, name)
end
end
return ret
end
---@return integer[]
function AI:getSelectedCards()
if not self:isInDashboard() then return Util.DummyTable end
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
return table.map(self.handler.selected_targets, function(pid)
return self.room:getPlayerById(pid)
end)
end
function AI:getSelectedSkill()
if not self:isInDashboard() then return nil end
return self.handler.skill_name
end
function AI:selectCard(cid, selected)
if not self:isInDashboard() then return end
verbose("%s选择卡牌%d(%s)", selected and "" or "取消", cid, tostring(Fk:getCardById(cid)))
self.handler:update("CardItem", cid, "click", { selected = selected })
end
---@param player ServerPlayer
function AI:selectTarget(player, selected)
if not self:isInDashboard() then return end
verbose("%s选择角色%s", selected and "" or "取消", tostring(player))
self.handler:update("Photo", player.id, "click", { selected = selected })
end
function AI:selectSkill(skill_name, selected)
if not self:isInDashboard() then return end
verbose("%s选择技能%s", selected and "" or "取消", skill_name)
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()
end
function AI:isDeadend()
if not self:isInDashboard() then return true end
return (not self:okButtonEnabled()) and #self:getEnabledCards() == 0
and #self:getEnabledTargets() == 0
end
function AI:doOKButton()
if not self:isInDashboard() then return end
if not self:okButtonEnabled() then return "" end
return self.handler:doOKButton()
end
---@return Skill?
function AI:currentSkill()
local room = self.room
if room.current_cost_skill then return room.current_cost_skill end
local sname = room.logic:getCurrentSkillName()
if sname then
return Fk.skills[sname]
end
end
function AI:makeReply()
Self = self.player
-- local start = os.getms()
local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel"
if ret == "" then ret = "__cancel" end
-- local to_delay = 500 - (os.getms() - start) / 1000
-- print(to_delay)
-- self.room:delay(to_delay)
local now = os.getms()
local fn = self["handle" .. self.command]
local is_active = self.command == "AskForUseActiveSkill"
if is_active then
local skill = Fk.skills[self.data[1]]
skill._extra_data = self.data[4]
end
local ret = "__cancel"
if fn then
local handler_class = Fk.request_handlers[self.command]
if handler_class then
self.handler = handler_class:new(self.player, self.data)
self.handler:setup()
end
ret = fn(self, self.data)
end
if ret == nil or ret == "" then ret = "__cancel" end
self.handler = nil
if is_active then
local skill = Fk.skills[self.data[1]]
skill._extra_data = Util.DummyTable
end
verbose("%s 在%.2fms后得出结果:%s", self.command, (os.getms() - now) / 1000, json.encode(ret))
return ret
end

View File

@ -2,20 +2,29 @@
AI = require "server.ai.ai"
TrustAI = require "server.ai.trust_ai"
RandomAI = require "server.ai.random_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")
local directories
if UsingNewCore then
directories = FileIO.ls("..")
require "standard_cards.ai"
require "standard.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

421
lua/server/ai/logic.lua Normal file
View File

@ -0,0 +1,421 @@
--- 用于对标room和room.logic的、专用于计算某一轮操作的收益的类。
---
--- 里面提供的方法和room尽可能完全一致以便自动分析与手工编写简易预测流程。
---@class AIGameLogic: Object
---@field public ai SmartAI
---@field public player ServerPlayer
---@field public benefit integer
local AIGameLogic = class("AIGameLogic")
---@param ai SmartAI
function AIGameLogic:initialize(ai, base_benefit)
self.benefit = base_benefit or 0
self.ai = ai
self.player = ai.player
self.logic = self -- 用于处理room.logic 这样真的好么。。
self.owner_map = ai.room.owner_map
self.card_place = ai.room.card_place
end
function AIGameLogic:__index(_)
return function() return Util.DummyTable end
end
function AIGameLogic:getPlayerById(id)
return self.ai.room:getPlayerById(id)
end
function AIGameLogic:trigger(event, target, data)
local ai = self.ai
local logic = ai.room.logic
local skills = logic.skill_table[event] or Util.DummyTable
local _target = ai.room.current -- for iteration
local player = _target
local exit
repeat
for _, skill in ipairs(skills) do
local skill_ai = fk.ai_trigger_skills[skill.name]
if skill_ai then
exit = skill_ai:getCorrect(self, event, target, player, data)
if exit then break end
end
end
if exit then break end
player = player.next
until player == _target
return exit
end
--- 血条、翻面、铁索等等的收益论瞎jb乱填版
function AIGameLogic:setPlayerProperty(player, key, value)
local orig = player[key]
local benefit = 0
if key == "hp" then
benefit = (value - orig) * 200
elseif key == "shield" then
benefit = (value - orig) * 150
elseif key == "chained" then
benefit = value and -80 or 80
elseif key == "faceup" then
if value and not orig then
benefit = 330
elseif orig and not value then
benefit = -330
end
end
if self.ai:isEnemy(player) then benefit = -benefit end
self.benefit = self.benefit + benefit
end
--- 牌差收益论瞎jb乱填版
--- 根据moveInfo判断玩家是拿牌还是掉牌进而暴力算收益
---@param data CardsMoveStruct
---@param info MoveInfo
function AIGameLogic:applyMoveInfo(data, info)
local benefit = 0
if data.from then
if info.fromArea == Player.Hand then
benefit = -90
elseif info.fromArea == Player.Equip then
benefit = -110
elseif info.fromArea == Player.Judge then
benefit = 180
elseif info.fromArea == Player.Special then
benefit = -60
end
local from = data.from and self:getPlayerById(data.from)
if from and self.ai:isEnemy(from) then benefit = -benefit end
self.benefit = self.benefit + benefit
benefit = 0
end
if data.to then
if data.toArea == Player.Hand then
benefit = 90
elseif data.toArea == Player.Equip then
benefit = 110
elseif data.toArea == Player.Judge then
benefit = -180
elseif data.toArea == Player.Special then
benefit = 60
end
local to = data.to and self:getPlayerById(data.to)
if to and self.ai:isEnemy(to) then benefit = -benefit end
self.benefit = self.benefit + benefit
end
end
--- 阉割版GameEvent: 专用于AI进行简单的收益推理。
---
--- 事件首先需要定义自己对于某某玩家的基础收益值,例如伤害事件对目标造成-200的
--- 收益。事件还要定义自己包含的触发时机列表,根据时机列表考虑相关技能对本次
--- 事件的收益修正,最终获得真正的收益值。
---
--- 事件用于即将选卡/选目标时或者触发技AI思考自己对某事件影响时构造并计算收益
--- 因此很容易发生事件嵌套现象。为防止AI思考过久必须对事件嵌套层数加以限制
--- 比如限制最多思考到两层嵌套毕竟没算力只能让AI蠢点了
---@class AIGameEvent: Object
---@field public ai SmartAI
---@field public logic AIGameLogic
---@field public player ServerPlayer
---@field public data any
local AIGameEvent = class("AIGameEvent")
---@param ai_logic AIGameLogic
function AIGameEvent:initialize(ai_logic, ...)
self.room = ai_logic
self.logic = ai_logic
self.ai = ai_logic.ai
self.player = self.ai.player
self.data = { ... }
end
-- 真正的收益计算函数:子类重写这个
function AIGameEvent:exec()
end
local _depth = 0
-- 用做API的收益计算函数不要重写
function AIGameEvent:getBenefit()
local ret = true
_depth = _depth + 1
if _depth <= 30 then
ret = self:exec()
end
_depth = _depth - 1
return ret
end
-- hp.lua
local ChangeHp = AIGameEvent:subclass("AIGameEvent.ChangeHp")
fk.ai_events.ChangeHp = ChangeHp
function ChangeHp:exec()
local logic = self.logic
local player, num, reason, skillName, damageStruct = table.unpack(self.data)
---@type HpChangedData
local data = {
num = num,
reason = reason,
skillName = skillName,
damageEvent = damageStruct,
}
if logic:trigger(fk.BeforeHpChanged, player, data) then
return true
end
logic:setPlayerProperty(player, "hp", math.min(player.hp + data.num, player.maxHp))
logic:trigger(fk.HpChanged, player, data)
end
function AIGameLogic:changeHp(player, num, reason, skillName, damageStruct)
return not ChangeHp:new(self, player, num, reason, skillName, damageStruct):getBenefit()
end
local Damage = AIGameEvent:subclass("AIGameEvent.Damage")
fk.ai_events.Damage = Damage
function Damage:exec()
local logic = self.logic
local damageStruct = table.unpack(self.data)
if (not damageStruct.chain) and (not damageStruct.chain_table) and Fk:canChain(damageStruct.damageType) then
damageStruct.chain_table = table.filter(self.ai.room:getOtherPlayers(damageStruct.to), function(p)
return p.chained
end)
end
local stages = {}
if not damageStruct.isVirtualDMG then
stages = {
{ fk.PreDamage, "from"},
{ fk.DamageCaused, "from" },
{ fk.DamageInflicted, "to" },
}
end
for _, struct in ipairs(stages) do
local event, player = table.unpack(struct)
if logic:trigger(event, damageStruct[player], damageStruct) then
return true
end
if damageStruct.damage < 1 then return true end
end
if not damageStruct.isVirtualDMG then
ChangeHp:new(logic, damageStruct.to, -damageStruct.damage,
"damage", damageStruct.skillName, damageStruct):getBenefit()
end
logic:trigger(fk.Damage, damageStruct.from, damageStruct)
logic:trigger(fk.Damaged, damageStruct.to, damageStruct)
logic:trigger(fk.DamageFinished, damageStruct.to, damageStruct)
if damageStruct.chain_table and #damageStruct.chain_table > 0 then
for _, p in ipairs(damageStruct.chain_table) do
local dmg = {
from = damageStruct.from,
to = p,
damage = damageStruct.damage,
damageType = damageStruct.damageType,
card = damageStruct.card,
skillName = damageStruct.skillName,
chain = true,
}
Damage:new(logic, dmg):getBenefit()
end
end
end
function AIGameLogic:damage(damageStruct)
return not Damage:new(self, damageStruct):getBenefit()
end
local LoseHp = AIGameEvent:subclass("AIGameEvent.LoseHp")
fk.ai_events.LoseHp = LoseHp
LoseHp.exec = AIParser.parseEventFunc(GameEvent.LoseHp.main)
function AIGameLogic:loseHp(player, num, skillName)
return not LoseHp:new(self, player, num, skillName):getBenefit()
end
local Recover = AIGameEvent:subclass("AIGameEvent.Recover")
fk.ai_events.Recover = Recover
Recover.exec = AIParser.parseEventFunc(GameEvent.Recover.main)
function AIGameLogic:recover(recoverStruct)
return not Recover:new(self, recoverStruct):getBenefit()
end
-- skill.lua
local SkillEffect = AIGameEvent:subclass("AIGameEvent.SkillEffect")
fk.ai_events.SkillEffect = SkillEffect
function SkillEffect:exec()
local logic = self.logic
local effect_cb, player, skill, skill_data = table.unpack(self.data)
local main_skill = skill.main_skill and skill.main_skill or skill
logic:trigger(fk.SkillEffect, player, main_skill)
effect_cb()
logic:trigger(fk.AfterSkillEffect, player, main_skill)
end
function AIGameLogic:useSkill(player, skill, effect_cb, skill_data)
return not SkillEffect:new(self, effect_cb, player, skill, skill_data or Util.DummyTable):getBenefit()
end
-- movecard.lua
local MoveCards = AIGameEvent:subclass("AIGameEvent.MoveCards")
fk.ai_events.MoveCards = MoveCards
function MoveCards:exec()
local args = self.data
local logic = self.logic
local cardsMoveStructs = {}
for _, cardsMoveInfo in ipairs(args) do
if #cardsMoveInfo.ids > 0 then
---@type MoveInfo[]
local infos = {}
for _, id in ipairs(cardsMoveInfo.ids) do
table.insert(infos, {
cardId = id,
fromArea = cardsMoveInfo.fromArea or self.ai.room:getCardArea(id),
fromSpecialName = cardsMoveInfo.from and logic:getPlayerById(cardsMoveInfo.from):getPileNameOfId(id),
})
end
cardsMoveInfo.moveInfo = infos
table.insert(cardsMoveStructs, cardsMoveInfo)
end
end
if logic:trigger(fk.BeforeCardsMove, nil, cardsMoveStructs) then
return true
end
for _, data in ipairs(cardsMoveStructs) do
for _, info in ipairs(data.moveInfo) do
logic:applyMoveInfo(data, info)
end
end
logic:trigger(fk.AfterCardsMove, nil, cardsMoveStructs)
end
function AIGameLogic:getNCards(num, from)
local cardIds = {}
for _ = 1, num do
table.insert(cardIds, 1)
end
return cardIds
end
function AIGameLogic:moveCards(...)
return not MoveCards:new(self, ...):getBenefit()
end
AIGameLogic.moveCardTo = GameEventWrappers.moveCardTo
AIGameLogic.obtainCard = GameEventWrappers.obtainCard
AIGameLogic.drawCards = GameEventWrappers.drawCards
AIGameLogic.throwCard = GameEventWrappers.throwCard
function AIGameLogic:recastCard(card_ids, who, skillName)
if type(card_ids) == "number" then
card_ids = {card_ids}
end
skillName = skillName or "recast"
self:moveCards({
ids = card_ids,
from = who.id,
toArea = Card.DiscardPile,
skillName = skillName,
moveReason = fk.ReasonRecast,
proposer = who.id
})
return self:drawCards(who, #card_ids, skillName)
end
-- usecard.lua
local UseCard = AIGameEvent:subclass("AIGameEvent.UseCard")
fk.ai_events.UseCard = UseCard
function UseCard:exec()
local ai = self.ai
local room = ai.room
local logic = self.logic
local cardUseEvent = table.unpack(self.data)
if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then
return true
end
logic:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse)
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.CardUsing }) do
if not cardUseEvent.toCard and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
break
end
logic:trigger(event, room:getPlayerById(cardUseEvent.from), cardUseEvent)
if event == fk.CardUsing then
logic:doCardUseEffect(cardUseEvent)
end
end
logic:trigger(fk.CardUseFinished, room:getPlayerById(cardUseEvent.from), cardUseEvent)
logic:moveCards{
fromArea = Card.Processing,
toArea = Card.DiscardPile,
ids = Card:getIdList(cardUseEvent.card),
moveReason = fk.ReasonUse,
}
end
function AIGameLogic:useCard(cardUseEvent)
return not UseCard:new(self, cardUseEvent):getBenefit()
end
function AIGameLogic:deadPlayerFilter(playerIds)
local newPlayerIds = {}
for _, playerId in ipairs(playerIds) do
if self:getPlayerById(playerId):isAlive() then
table.insert(newPlayerIds, playerId)
end
end
return newPlayerIds
end
AIGameLogic.doCardUseEffect = GameEventWrappers.doCardUseEffect
local CardEffect = AIGameEvent:subclass("AIGameEvent.CardEffect")
fk.ai_events.CardEffect = CardEffect
CardEffect.exec = AIParser.parseEventFunc(GameEvent.CardEffect.main)
function AIGameLogic:doCardEffect(cardEffectEvent)
return not CardEffect:new(self, cardEffectEvent):getBenefit()
end
function AIGameLogic:handleCardEffect(event, cardEffectEvent)
-- 不考虑闪与无懈 100%生效
-- 闪和无懈早该重构重构了
if event == fk.CardEffecting then
if cardEffectEvent.card.skill then
SkillEffect:new(self, function()
local skill = cardEffectEvent.card.skill
local ai = fk.ai_skills[skill.name]
if ai then
ai:onEffect(self, cardEffectEvent)
end
end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill):getBenefit()
end
end
end
return AIGameLogic, AIGameEvent

View File

@ -1,7 +0,0 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
local MonteCarlo = class("MonteCarlo")
return MonteCarlo

58
lua/server/ai/parser.lua Normal file
View File

@ -0,0 +1,58 @@
--- 用于从on_use/on_effect等函数自动生成AI推理用的模拟流程
---@class AIParser
local AIParser = {}
---@type table<string, string[]> 文件名-lines
local loaded_files = {}
local function getLines(filename)
if loaded_files[filename] then return loaded_files[filename] end
if UsingNewCore then
if filename:startsWith("./lua") then
filename = "./packages/freekill-core/" .. filename
end
FileIO.cd("../..")
end
local t = {}
for line in io.lines(filename) do
table.insert(t, line)
end
loaded_files[filename] = t
if UsingNewCore then
FileIO.cd("packages/freekill-core")
end
return t
end
local function getFunctionSource(fn)
local info = debug.getinfo(fn, "S")
local lines = getLines(info.short_src)
return table.slice(lines, info.linedefined, info.lastlinedefined + 1)
end
-- 最简单替换breakEvent改成return
function AIParser.parseEventFunc(fn)
local sources = getFunctionSource(fn)
local parsed = {}
for i, line in ipairs(sources) do
if i == 1 then
table.insert(parsed, "return function(self)")
else
if line:find(":breakEvent%(") then
line = "return true"
end
table.insert(parsed, line)
end
end
return load(table.concat(parsed, '\n'))()
end
function AIParser.parseEventWrapper(wrapperFn)
local sources = getFunctionSource(wrapperFn)
print(table.concat(sources, "\n"))
end
return AIParser

View File

@ -1,405 +0,0 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---@class RandomAI: AI
local RandomAI = AI:subclass("RandomAI")
---@param self RandomAI
---@param skill ActiveSkill
---@param card? Card
---@param extra_data? table
function RandomAI:useActiveSkill(skill, card, extra_data)
local room = self.room
local player = self.player
extra_data = extra_data or Util.DummyTable
if skill:isInstanceOf(ViewAsSkill) then return "" end
if self.command == "PlayCard" and (not skill:canUse(player, card) or (card and player:prohibitUse(card))) then
return ""
end
local interaction_data
if skill and skill.interaction then
skill.interaction.data = nil
interaction_data = skill:interaction()
if type(interaction_data) == "table" then
if interaction_data.type == "spin" then
interaction_data = math.random(interaction_data.from, interaction_data.to)
elseif interaction_data.type == "combo" then
interaction_data = interaction_data.default
else
-- use default data when handling custom interaction
interaction_data = interaction_data.default or interaction_data.default_choice or nil
end
end
if interaction_data == nil then return "" end
skill.interaction.data = interaction_data
end
local max_try_times = 100
local selected_targets = {}
local selected_cards = {}
-- TODO: ng that 'must_targets' & 'exclusive_targets' should be rebuilt later
local limited_targets = {}
for _, name in ipairs({"must_targets","exclusive_targets","include_targets"}) do
if type(extra_data[name]) == "table" then
table.insertTableIfNeed(limited_targets, extra_data[name])
end
end
local all_cards = player:getCardIds{ Player.Hand, Player.Equip }
if skill.expand_pile then
if type(skill.expand_pile) == "string" then
table.insertTableIfNeed(all_cards, player.special_cards[skill.expand_pile] or {})
elseif type(skill.expand_pile) == "table" then
table.insertTableIfNeed(all_cards, skill.expand_pile)
end
end
--local max_target_num = skill:getMaxTargetNum(player, card)
local card_filter_func = card and Util.FalseFunc or skill.cardFilter
local firstTry
for _ = 0, max_try_times do
if not firstTry and skill:feasible(selected_targets, selected_cards, self.player, card) then
firstTry = {table.simpleClone(selected_targets), table.simpleClone(selected_cards)}
end
if firstTry and math.random() < 0.1 then break end
local avail_targets = table.filter(room.alive_players, function(p)
return not table.contains(selected_targets, p.id) and (#limited_targets == 0 or table.contains(limited_targets, p.id))
and skill:targetFilter(p.id, selected_targets, selected_cards, card)
and (not card or not player:isProhibited(p, card))
end)
local avail_cards = table.filter(all_cards, function(id)
return not table.contains(selected_cards, id) and card_filter_func(skill, id, selected_cards, selected_targets)
end)
local random_list = table.connect(avail_targets, avail_cards)
if #random_list == 0 then break end
local randomIndex = math.random(#random_list)
if randomIndex <= #avail_targets then
table.insertIfNeed(selected_targets, random_list[randomIndex].id)
else
table.insertIfNeed(selected_cards, random_list[randomIndex])
end
end
local feasibleCheck = function () return skill:feasible(selected_targets, selected_cards, self.player, card) end
if firstTry and not feasibleCheck() then
selected_targets = firstTry[1]
selected_cards = firstTry[2]
end
if feasibleCheck() then
local ret = json.encode{
card = card and card.id or json.encode{
skill = skill.name,
subcards = selected_cards,
},
targets = selected_targets,
interaction_data = interaction_data,
}
return ret
end
return ""
end
---@param skill ViewAsSkill
---@param pattern? string @ no 'pattern' means it needs to pass the 'canUse' check
---@param cancelable? bool
---@param extra_data? table
---@param cardResponsing? bool
function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data, cardResponsing)
local player = self.player
local room = self.room
local precondition
cancelable = cancelable or (cancelable == nil)
extra_data = extra_data or Util.DummyTable
if not skill then return "" end
if not pattern then
precondition = skill:enabledAtPlay(player)
if not precondition then return "" end
local exp = Exppattern:Parse(skill.pattern)
local cnames = {}
for _, m in ipairs(exp.matchers) do
if m.name then table.insertTable(cnames, m.name) end
end
for _, n in ipairs(cnames) do
local c = Fk:cloneCard(n)
precondition = c.skill:canUse(player, c, extra_data)
if precondition then break end
end
else
precondition = skill:enabledAtResponse(player, cardResponsing) and Exppattern:Parse(pattern):matchExp(skill.pattern)
end
if (not precondition) or (cancelable and math.random() < 0.2) then return "" end
local interaction_data
if skill.interaction then
skill.interaction.data = nil
interaction_data = skill:interaction()
if type(interaction_data) == "table" then
if interaction_data.type == "spin" then
interaction_data = math.random(interaction_data.from, interaction_data.to)
elseif interaction_data.type == "combo" then
interaction_data = interaction_data.default
else
-- use default data when handling custom interaction
interaction_data = interaction_data.default or interaction_data.default_choice or nil
end
end
if interaction_data == nil then return "" end
skill.interaction.data = interaction_data
end
local selected_cards = {}
local selected_targets = {}
local card
local max_try_time = 100
local all_cards = player:getCardIds{ Player.Hand, Player.Equip }
if skill.expand_pile then
if type(skill.expand_pile) == "string" then
table.insertTableIfNeed(all_cards, player.special_cards[skill.expand_pile] or {})
elseif type(skill.expand_pile) == "table" then
table.insertTableIfNeed(all_cards, skill.expand_pile)
end
end
for _ = 0, max_try_time do
card = skill:viewAs(selected_cards)
if card then break end
local avail_cards = table.filter(all_cards, function(id)
return not table.contains(selected_cards, id) and skill:cardFilter(id, selected_cards)
end)
if #avail_cards == 0 then break end
table.insert(selected_cards, table.random(avail_cards))
end
if not card then return "" end
if cardResponsing then
if not player:prohibitResponse(card) then
return json.encode{
card = json.encode{
skill = skill.name,
subcards = selected_cards,
},
targets = {},
interaction_data = interaction_data,
}
end
return ""
end
if player:prohibitUse(card) then return "" end
if pattern or player:canUse(card, extra_data) then
local limited_targets = {}
for _, name in ipairs({"must_targets","exclusive_targets","include_targets"}) do
if type(extra_data[name]) == "table" then
table.insertTableIfNeed(limited_targets, extra_data[name])
end
end
for _ = 0, max_try_time do
if card.skill:feasible(selected_targets, selected_cards, player, card) then break end
local avail_targets = table.filter(room.alive_players, function(p)
return not table.contains(selected_targets, p.id) and (#limited_targets == 0 or table.contains(limited_targets, p.id))
and card.skill:targetFilter(p.id, selected_targets, selected_cards, card, extra_data)
and not player:isProhibited(p, card)
end)
if #avail_targets == 0 then break end
table.insert(selected_targets, table.random(avail_targets).id)
end
if card.skill:feasible(selected_targets, selected_cards, player, card, extra_data) then
local ret = json.encode{
card = json.encode{
skill = skill.name,
subcards = selected_cards,
},
targets = selected_targets,
interaction_data = interaction_data,
}
return ret
end
end
return ""
end
---@type table<string, fun(self: RandomAI, jsonData: string): string>
local random_cb = {}
random_cb["AskForUseActiveSkill"] = function(self, jsonData)
local data = json.decode(jsonData)
local skill = Fk.skills[data[1]]
if not skill then return "" end
local cancelable = data[3]
if cancelable and math.random() < 0.25 then return "" end
local extra_data = data[4]
for k, v in pairs(extra_data) do
skill[k] = v
end
if skill:isInstanceOf(ViewAsSkill) then
return self:useVSSkill(skill, nil, cancelable, extra_data)
end
local player = self.player
if skill.name == "choose_cards_skill" then
local exp = Exppattern:Parse(extra_data.pattern)
local cards = table.filter(player:getCardIds(extra_data.include_equip and "he" or "h"), function(cid)
return exp:match(Fk:getCardById(cid))
end)
local maxNum = extra_data.num
local minNum = extra_data.min_num
cards = table.random(cards, math.random(minNum, maxNum))
return json.encode{
card = json.encode{
skill = skill.name,
subcards = cards,
},
targets = {},
}
end
return self:useActiveSkill(skill)
end
random_cb["AskForSkillInvoke"] = function(self, jsonData)
local skill_name, prompt = table.unpack(json.decode(jsonData))
local chance = 0.55
if Fk.skills[skill_name] ~= nil and self.player:hasSkill(skill_name) then
chance = 0.8
end
if math.random() < chance then
return "1"
end
return ""
end
random_cb["AskForChoice"] = function(self, jsonData)
local data = json.decode(jsonData)
local choices = data[1]
if table.contains(choices, "Cancel") and #choices > 1 and math.random() < 0.6 then
table.removeOne(choices, "Cancel")
end
return table.random(choices)
end
random_cb["AskForUseCard"] = function(self, jsonData)
local player = self.player
local data = json.decode(jsonData)
local card_name = data[1]
local pattern = data[2] or card_name
local prompt = data[3]
local cancelable = data[4]
local extra_data = data[5] or Util.DummyTable
if card_name == "peach" then
if type(extra_data.must_targets) == "table" and extra_data.must_targets[1] ~= player.id and math.random() < 0.8 then
return ""
end
end
if (cancelable and math.random() < 0.15) then return "" end
local cards = table.map(self.player:getCardIds("he&"), Util.Id2CardMapper)
local exp = Exppattern:Parse(pattern)
cards = table.filter(cards, function(c)
return exp:match(c) and not player:prohibitUse(c)
end)
local vss = table.filter(player:getAllSkills(), function(s)
return s:isInstanceOf(ViewAsSkill)
end)
table.insertTable(cards, vss)
while #cards > 0 do
local sth = table.remove(cards, math.random(#cards))
if sth:isInstanceOf(Card) then
local ret = self:useActiveSkill(sth.skill, sth, extra_data)
if ret ~= "" then return ret end
else
local ret = self:useVSSkill(sth, pattern, cancelable, extra_data)
if ret ~= "" then return ret end
end
end
return ""
end
random_cb["AskForResponseCard"] = function(self, jsonData)
local data = json.decode(jsonData)
local pattern = data[2]
local cancelable = data[4] or true
local extra_data = data[5] or Util.DummyTable
local player = self.player
local cards = table.map(self.player:getCardIds("he&"), Util.Id2CardMapper)
local exp = Exppattern:Parse(pattern)
cards = table.filter(cards, function(c)
return exp:match(c) and not player:prohibitResponse(c)
end)
local vss = table.filter(player:getAllSkills(), function(s)
return s:isInstanceOf(ViewAsSkill)
end)
table.insertTable(cards, vss)
while #cards > 0 do
local sth = table.remove(cards, math.random(#cards))
if sth:isInstanceOf(Card) then
return json.encode{ card = sth.id, targets = {} }
else
local ret = self:useVSSkill(sth, pattern, cancelable, extra_data, true)
if ret ~= "" then return ret end
end
end
return ""
end
random_cb["PlayCard"] = function(self, jsonData)
local cards = table.map(self.player:getCardIds("h&"), Util.Id2CardMapper)
local actives = table.filter(self.player:getAllSkills(), function(s)
return s:isInstanceOf(ActiveSkill)
end)
local vss = table.filter(self.player:getAllSkills(), function(s)
return s:isInstanceOf(ViewAsSkill)
end)
table.insertTable(cards, actives)
table.insertTable(cards, vss)
while #cards > 0 do
local sth = table.remove(cards, math.random(#cards))
if sth:isInstanceOf(Card) then
local card = sth
local skill = card.skill ---@type ActiveSkill
if math.random() > 0.15 then
local ret = RandomAI.useActiveSkill(self, skill, card)
if ret ~= "" then return ret end
end
elseif sth:isInstanceOf(ActiveSkill) then
local active = sth
if math.random() > 0.30 then
local ret = RandomAI.useActiveSkill(self, active, nil)
if ret ~= "" then return ret end
end
else
local vs = sth
if math.random() > 0.20 then
local ret = self:useVSSkill(vs)
if ret ~= "" then return ret end
end
end
end
return ""
end
-- FIXME: for smart ai
RandomAI.cb_table = random_cb
function RandomAI:initialize(player)
AI.initialize(self, player)
self.cb_table = random_cb
end
return RandomAI

82
lua/server/ai/skill.lua Normal file
View File

@ -0,0 +1,82 @@
--- 关于某个技能如何在AI中处理。
---
--- 相关方法分为三类,分别是如何搜索、如何计算收益、如何进行收益推理
---
--- 所谓搜索,就是如何确定下一步该选择哪张卡牌/哪名角色等。
--- 默认情况下AI选择收益最高的选项作为下一步如果遇到了死胡同就返回考虑下一种。
--- 所谓死胡同就是什么都不能点击,也不能点确定的状态,必须取消某些的选择。
---
--- 所谓的收益计算就是估算这个选项在这个技能的语境下,他大概会带来多少收益。
--- 限于算力,我们不能编写太复杂的收益计算。默认情况下,收益可以通过推理完成,
--- 而推理的步骤需要Modder向AI给出提示。
---
--- 所谓的给出提示就是上面的“如何进行收益推理”。拓展可以针对点击某张卡牌或者
--- 点击某个角色告诉AI这么点击了可能会发生某种事件。AI根据事件以及游戏内包含的
--- 其他技能进行计算,得出收益值。若不想让它这样计算,也可以在上一步直接指定
--- 固定的收益值。
---
--- 所谓的“可能发生某种事件”大致类似GameEvent但是内部功能大幅简化了因为
--- 只是用于简单的推理。详见同文件夹下event.lua内容。
---@class SkillAI: Object
---@field public skill ActiveSkill
local SkillAI = class("SkillAI")
--- 收益估计
---@param ai SmartAI
---@return integer?
function SkillAI:getEstimatedBenefit(ai) end
--- 要返回一个结果,以及收益值
---@param ai SmartAI
---@return any?, integer?
function SkillAI:think(ai) end
---@param skill string
function SkillAI:initialize(skill)
self.skill = Fk.skills[skill]
end
-- 搜索类方法:怎么走下一步?
---@param ai SmartAI
function SkillAI:chooseInteraction(ai) end
---@param ai SmartAI
function SkillAI:chooseCards(ai) end
---@param ai SmartAI
---@return any, integer?
function SkillAI:chooseTargets(ai) end
-- 流程模拟类方法为了让AIGameLogic开心
--- 对触发技生效的模拟
---@param logic AIGameLogic
---@param event Event @ TriggerEvent
---@param target ServerPlayer @ Player who triggered this event
---@param player ServerPlayer @ Player who is operating
---@param data any @ useful data of the event
function SkillAI:onTriggerUse(logic, event, target, player, data) end
--- 对主动技生效/卡牌被使用时的模拟
---@param logic AIGameLogic
---@param event CardUseStruct | SkillEffectEvent
function SkillAI:onUse(logic, event) end
--- 对卡牌生效的模拟
---@param logic AIGameLogic
---@param cardEffectEvent CardEffectEvent | SkillEffectEvent
function SkillAI:onEffect(logic, cardEffectEvent) end
--- 最后效仿一下fk_ex故事
---@class SkillAISpec
---@field estimated_benefit? integer|fun(self: SkillAI, ai: SmartAI): integer?
---@field think? fun(self: SkillAI, ai: SmartAI): any?, integer?
---@field choose_interaction? fun(self: SkillAI, ai: SmartAI): boolean?
---@field choose_cards? fun(self: SkillAI, ai: SmartAI): boolean?
---@field choose_targets? fun(self: SkillAI, ai: SmartAI): any, integer?
---@field on_trigger_use? fun(self: SkillAI, logic: AIGameLogic, event: Event, target: ServerPlayer?, player: ServerPlayer, data: any)
---@field on_use? fun(self: SkillAI, logic: AIGameLogic, effect: SkillEffectEvent | CardEffectEvent)
---@field on_effect? fun(self: SkillAI, logic: AIGameLogic, effect: SkillEffectEvent | CardEffectEvent)
return SkillAI

View File

@ -1,41 +1,32 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
--[[
SmartAI: AI架构的AI体系
AI常用的种种表以及实用函数等AI逻辑的接口
AI的核心在于编程实现对各种交互的回应(room:askForXXX)
smart_cb表以实现合理的答复
便AIAI判断时常用的函数
AI框架
1. smart_cb表
2.
3.
-- TODO: 优化底层逻辑防止AI每次操作之前都要json.decode一下。
-- TODO: 更加详细的文档
--]]
---@class SmartAI: AI
---@class SmartAI: TrustAI
---@field private _memory table<string, any> @ AI底层的空间换时间机制
---@field public friends ServerPlayer[] @ 队友
---@field public enemies ServerPlayer[] @ 敌人
local SmartAI = AI:subclass("SmartAI")
local SmartAI = TrustAI:subclass("SmartAI") -- 哦,我懒得写出闪之类的,不得不继承一下,饶了我吧
---@type table<string, fun(self: SmartAI, jsonData: string): string>
local smart_cb = {}
AIParser = require 'lua.server.ai.parser'
SkillAI = require "lua.server.ai.skill"
TriggerSkillAI = require "lua.server.ai.trigger_skill"
---@type table<string, AIGameEvent>
fk.ai_events = {}
AIGameLogic, AIGameEvent = require "lua.server.ai.logic"
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)
self._memory = setmetatable({}, { __mode = "k" })
return TrustAI.makeReply(self)
end
function SmartAI:__index(k)
@ -56,201 +47,214 @@ function SmartAI:__index(k)
return ret
end
-- AI框架中常用的模式化函数。
-- 先从表中选函数,若无则调用默认的。点点点是参数
function SmartAI:callFromTable(func_table, default_func, key, ...)
local f = func_table[key]
if type(f) == "function" then
return f(...)
elseif type(default_func) == "function" then
return default_func(...)
else
return nil
end
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
-- 考虑为triggerSkill设置收益修正函数
---@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,
--@field ask_use_card? fun(skill: ActiveSkill, ai: SmartAI): any
--@field ask_response? fun(skill: ActiveSkill, ai: SmartAI): any
---@type table<string, SkillAI>
fk.ai_skills = {}
---@param key string
---@param spec SkillAISpec
---@param inherit? string
function SmartAI.static:setSkillAI(key, spec, inherit)
if not fk.ai_skills[key] then
fk.ai_skills[key] = SkillAI:new(key)
end
local ai = fk.ai_skills[key]
local qsgs_wisdom_map = {
estimated_benefit = "getEstimatedBenefit",
think = "think",
choose_interaction = "chooseInteraction",
choose_cards = "chooseCards",
choose_targets = "chooseTargets",
on_trigger_use = "onTriggerUse",
on_use = "onUse",
on_effect = "onEffect",
}
if inherit then
local ai2 = fk.ai_skills[inherit]
for _, k in pairs(qsgs_wisdom_map) do
ai[k] = ai2[k]
end
end
for k, v in pairs(spec) do
local key2 = qsgs_wisdom_map[k]
if key2 then ai[key2] = type(v) == "function" and v or function() return v end end
end
end
-- AskForUseActiveSkill: 询问发动主动技/视为技
-- * 此处 UseReply.card 必定由 json.encode 而来
-- * 且原型为 { skill = skillName, subcards = integer[] }
----------------------------------------------------------
---@type table<string, fun(self: SmartAI, prompt: string, cancelable?: boolean, data: any): UseReply?>
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)
--- 将spec中的键值保存到这个技能的ai中
---@param key string
---@param spec SkillAISpec 表
---@param inherit? string 可以直接复用某个技能已有的函数 自然spec中更加优先
---@diagnostic disable-next-line
function SmartAI:setSkillAI(key, spec, inherit)
error("This is a static method. Please use SmartAI:setSkillAI(...)")
end
-- AskForUseCard: 询问使用卡牌
-- 判断函数一样返回UseReply此时卡牌可能是integer或者string
-- 为string的话肯定是由ViewAsSkill转化而来
-- 真的要考虑ViewAsSkill吗害怕
---------------------------------------------------------
-- 使用牌相关——同时见于主动使用和响应式使用。
--- 键是prompt的第一项或者牌名优先prompt其次name实在不行trueName。
---@type table<string, fun(self: SmartAI, pattern: string, prompt: string, cancelable?: boolean, extra_data?: UseExtraData): UseReply?>
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)
SmartAI:setSkillAI("__card_skill", {
choose_targets = function(self, ai)
local targets = ai:getEnabledTargets()
local logic = AIGameLogic:new(ai)
local val_func = function(p)
logic.benefit = 0
logic:useCard({
from = ai.player.id,
tos = { { p.id } },
card = ai:getSelectedCard(),
})
verbose("目前状况下,对%s的预测收益为%d", tostring(p), logic.benefit)
return logic.benefit
end
for _, p, val in fk.sorted_pairs(targets, val_func) do
if val > 0 then
ai:selectTarget(p, true)
return ai:doOKButton(), val
else
break
end
end
end,
think = function(self, ai)
local skill_name = self.skill.name
local pattern = skill_name:sub(1, #skill_name - 6)
local cards = ai:getEnabledCards(pattern)
cards = table.random(cards, math.min(#cards, 5)) --[[@as integer[] ]]
-- local cid = table.random(cards)
local best_ret, best_val = nil, -100000
for _, cid in ipairs(cards) do
ai:selectCard(cid, true)
local ret, val = self:chooseTargets(ai)
val = val or -100000
if not best_ret or (best_val < val) then
best_ret, best_val = ret, val
end
ai:unSelectAll()
end
return best_ret, best_val
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)
function SmartAI.static:setCardSkillAI(key, spec)
SmartAI:setSkillAI(key, spec, "__card_skill")
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:setCardSkillAI(key, spec, "__card_skill")
---@param key string
---@param spec SkillAISpec 表
function SmartAI:setCardSkillAI(key, spec)
error("This is a static method. Please use SmartAI:setCardSkillAI(...)")
end
-- AskForResponseCard: 询问打出卡牌
-- 注意事项同前
-------------------------------------
---@type table<string, TriggerSkillAI>
fk.ai_trigger_skills = {}
-- 一样的牌名或者prompt做键优先prompt
---@type table<string, fun(self: SmartAI, pattern: string, prompt: string, cancelable?: boolean, extra_data: any): UseReply?>
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)
---@param spec TriggerSkillAISpec
function SmartAI.static:setTriggerSkillAI(key, spec)
if not fk.ai_trigger_skills[key] then
fk.ai_trigger_skills[key] = TriggerSkillAI:new(key)
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: 视为技
-- 视为技对应的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)
if ret then return json.encode(ret) end
local ai = fk.ai_trigger_skills[key]
if spec.correct_func then
ai.getCorrect = spec.correct_func
end
end
--- 将spec中的键值保存到这个技能的ai中
---@param key string
---@param spec TriggerSkillAISpec
---@diagnostic disable-next-line
function SmartAI:setTriggerSkillAI(key, spec)
error("This is a static method. Please use SmartAI:setTriggerSkillAI(...)")
end
---@param cid_or_skill integer|string
function SmartAI:getBasicBenefit(cid_or_skill)
end
local function hasKey(t1, t2, key)
if (t1 and t1[key]) or (t2 and t2[key]) then return true end
end
local function callFromTables(tab, backup, key, ...)
local fn
if tab and tab[key] then
fn = tab[key]
elseif backup and backup[key] then
fn = backup[key]
end
if not fn then return end
return fn(...)
end
function SmartAI:handleAskForUseActiveSkill()
local name = self.handler.skill_name
local current_skill = self:currentSkill()
local ai
if current_skill then ai = fk.ai_skills[current_skill.name] end
if not ai then ai = fk.ai_skills[name] end
if not ai then return "" end
return ai:think(self)
end
function SmartAI:handlePlayCard()
local card_ids = self:getEnabledCards()
local skill_ai_list = {}
for _, id in ipairs(card_ids) do
local cd = Fk:getCardById(id)
local ai = fk.ai_skills[cd.skill.name]
if ai then
table.insertIfNeed(skill_ai_list, ai)
end
end
for _, sname in ipairs(self:getEnabledSkills()) do
local ai = fk.ai_skills[sname]
if ai then
table.insertIfNeed(skill_ai_list, ai)
end
end
verbose("======== %s: 开始计算出牌阶段 ========", tostring(self))
verbose("待选技能:[%s]", table.concat(table.map(skill_ai_list, function(ai) return ai.skill.name end), ", "))
local value_func = function(ai)
if not ai then return -500 end
local val = ai:getEstimatedBenefit(self)
return val or 0
end
local cancel_val = math.min(-90 * (self.player:getMaxCards() - self.player:getHandcardNum()), 0)
local best_ret, best_val
for _, ai, val in fk.sorted_pairs(skill_ai_list, value_func) do
verbose("[*] 考虑 %s (预估收益%d)", ai.skill.name, val)
if val < cancel_val then
verbose("由于预估收益小于取消的收益,不再思考")
break
end
local ret, real_val = ai:think(self)
-- if ret and ret ~= "" then return ret end
if not best_ret or (best_val < real_val) then
best_ret, best_val = ret, real_val
end
self:unSelectAll()
end
if best_ret and best_ret ~= "" then return best_ret end
return ""
end
@ -265,24 +269,33 @@ end
-- 函数返回true或者false即可。
-----------------------------
---@type table<string, boolean | fun(self: SmartAI, extra_data: any, prompt: string): bool>
fk.ai_skill_invoke = {}
--[[
---@type table<string, boolean | fun(self: SmartAI, prompt: string): bool>
fk.ai_skill_invoke = { AskForLuckCard = false }
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]
local skill = Fk.skills[skillName]
local spec = fk.ai_skills[skillName]
local ask
if spec then
ask = spec.skill_invoke
else
ask = fk.ai_skill_invoke[skillName]
end
if type(ask) == "function" then
return ask(self, prompt) and "1" or ""
return ask(skill, self) 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)
return math.random() < 0.5 and "1" or ""
end
end
--]]
-- 敌友判断相关。
-- 目前才开始,做个明身份打牌的就行了。
@ -309,38 +322,7 @@ 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

View File

@ -0,0 +1,29 @@
--- 关于某个触发技在AI中如何影响基于事件的收益推理。
---
--- 类似于真正的触发技这种技能AI也需要指定触发时机以及在某个时机之下
--- 如何进行收益计算。收益计算中亦可返回true表明事件被这个技能终止
--- 也就是不再进行后续其他技能的计算。
---
--- 触发技本身又会不断触发新的事件,比如刚烈反伤、反馈拿牌等。对于衍生事件
--- 亦可进一步进行推理但是AI会限制自己的搜索深度所以推理结果不一定准确。
---@class TriggerSkillAI
---@field public skill TriggerSkill
local TriggerSkillAI = class("TriggerSkillAI")
---@param skill string
function TriggerSkillAI:initialize(skill)
self.skill = Fk.skills[skill]
end
--- 获取触发技对收益评测的影响通过基于logic触发更多模拟事件来模拟收益的变化
---
--- 返回true表示打断后续收益判断逻辑
---@return boolean?
function TriggerSkillAI:getCorrect(logic, event, target, player, data)
end
---@class TriggerSkillAISpec
---@field correct_func fun(self: TriggerSkillAI, logic: AIGameLogic, event: Event, target: ServerPlayer?, player: ServerPlayer, data: any): boolean?
return TriggerSkillAI

View File

@ -1,15 +1,47 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
-- Trust AI
-- 需要打出牌时,有的话就打出
-- 需要使用闪、对自己使用无懈、酒、桃时,只要有就使用
-- 除此之外什么都不做
---@class TrustAI: AI
local TrustAI = AI:subclass("TrustAI")
local trust_cb = {}
function TrustAI:initialize(player)
AI.initialize(self, player)
self.cb_table = trust_cb
end
function TrustAI:handleAskForUseCard(data)
local pattern = data[2]
local prompt = data[3]
local wontuse = true
if pattern == "jink" then
wontuse = false
elseif pattern == "nullification" then
wontuse = prompt:split(":")[3] ~= tostring(self.player.id)
elseif pattern == "peach" or pattern == "peach,analeptic" then
wontuse = not prompt:startsWith("#AskForPeachesSelf")
end
if wontuse then return "" end
local cards = self:getEnabledCards()
for _, cd in ipairs(cards) do
self:selectCard(cd, true) -- 默认按下卡牌后直接可确定 懒得管了
return self:doOKButton()
end
return ""
end
function TrustAI:handleAskForResponseCard(data)
-- local cancelable = data[4] -- 算了,不按取消
local cards = self:getEnabledCards()
for _, cd in ipairs(cards) do
self:selectCard(cd, true) -- 默认按下卡牌后直接可确定 懒得管了
return self:doOKButton()
end
return ""
end
return TrustAI

View File

@ -48,6 +48,7 @@ fk.FinishJudge = 27
fk.RoundStart = 28
fk.RoundEnd = 29
fk.AfterRoundEnd = 85
fk.BeforeTurnOver = 79
fk.TurnedOver = 30
@ -126,7 +127,7 @@ fk.AfterSkillEffect = 82
-- 83 = PreTurnStart
-- 84 = AfterTurnEnd
-- 85 = xxx
-- 85 = AfterRoundEnd
-- 86 = AfterPhaseEnd
fk.AreaAborted = 87

View File

@ -80,11 +80,11 @@ function DrawInitial:main()
return
end
room:notifyMoveFocus(room.alive_players, "AskForLuckCard")
local request = Request:new("AskForSkillInvoke", room.alive_players)
local request = Request:new(room.alive_players, "AskForSkillInvoke")
for _, p in ipairs(room.alive_players) do
request:setData(p, { "AskForLuckCard", "#AskForLuckCard:::" .. room.settings.luckTime })
end
request.focus_text = "AskForLuckCard"
request.luck_data = luck_data
request.accept_cancel = true
request:ask()
@ -101,28 +101,22 @@ local Round = GameEvent:subclass("GameEvent.Round")
function Round:action()
local room = self.room
local p
local nextTurnOwner
local skipRoundPlus = false
repeat
nextTurnOwner = nil
skipRoundPlus = false
p = room.current
GameEvent.Turn:create(p):exec()
local currentPlayer
while true do
GameEvent.Turn:create(room.current):exec()
if room.game_finished then break end
local changingData = { from = room.current, to = room.current:getNextAlive(true, nil, true), skipRoundPlus = false }
local changingData = { from = room.current, to = room.current.next, skipRoundPlus = false }
room.logic:trigger(fk.EventTurnChanging, room.current, changingData, true)
skipRoundPlus = changingData.skipRoundPlus
local nextAlive = room.current:getNextAlive(true, nil, true)
if nextAlive ~= changingData.to and not changingData.to.dead then
room.current = changingData.to
nextTurnOwner = changingData.to
local nextTurnOwner = changingData.to
if room.current.seat > nextTurnOwner.seat and not changingData.skipRoundPlus then
break
else
room.current = nextAlive
room.current = nextTurnOwner
end
end
until p.seat >= (nextTurnOwner or p:getNextAlive(true, nil, true)).seat and not skipRoundPlus
end
function Round:main()
@ -153,7 +147,8 @@ function Round:main()
logic:trigger(fk.RoundStart, room.current)
self:action()
logic:trigger(fk.RoundEnd, p)
logic:trigger(fk.RoundEnd, room.current)
logic:trigger(fk.AfterRoundEnd, room.current)
end
function Round:clear()
@ -188,7 +183,10 @@ local Turn = GameEvent:subclass("GameEvent.Turn")
function Turn:prepare()
local room = self.room
local logic = room.logic
local player = room.current
local player = self.data[1]---@type ServerPlayer
if self.data[2] == nil then self.data[2] = {} end
local data = self.data[2]---@type TurnStruct
data.reason = data.reason or "game_rule"
if player.rest > 0 and player.rest < 999 then
room:setPlayerRest(player, player.rest - 1)
@ -208,21 +206,24 @@ function Turn:prepare()
return true
end
return logic:trigger(fk.BeforeTurnStart, player)
return logic:trigger(fk.BeforeTurnStart, player, data)
end
function Turn:main()
local room = self.room
room.current.phase = Player.PhaseNone
room.logic:trigger(fk.TurnStart, room.current)
room.current.phase = Player.NotActive
room.current:play()
local player = self.data[1]---@type ServerPlayer
local data = self.data[2]---@type TurnStruct
player.phase = Player.PhaseNone
room.logic:trigger(fk.TurnStart, player, data)
player.phase = Player.NotActive
player:play(data.phase_table)
end
function Turn:clear()
local room = self.room
local current = self.data[1]---@type ServerPlayer
local data = self.data[2]---@type TurnStruct
local current = room.current
local logic = room.logic
if self.interrupted then
if current.phase ~= Player.NotActive then
@ -239,8 +240,8 @@ function Turn:clear()
end
current.phase = Player.PhaseNone
logic:trigger(fk.TurnEnd, current, nil, self.interrupted)
logic:trigger(fk.AfterTurnEnd, current, nil, self.interrupted)
logic:trigger(fk.TurnEnd, current, data, self.interrupted)
logic:trigger(fk.AfterTurnEnd, current, data, self.interrupted)
current.phase = Player.NotActive
room:setTag("endTurn", false)
@ -275,7 +276,7 @@ function Phase:main()
local room = self.room
local logic = room.logic
local player = self.data[1] ---@type Player
local player = self.data[1] ---@type ServerPlayer
if not logic:trigger(fk.EventPhaseStart, player) then
if player.phase ~= Player.NotActive then
logic:trigger(fk.EventPhaseProceeding, player)
@ -322,7 +323,7 @@ function Phase:main()
}
room.logic:trigger(fk.DrawNCards, player, data)
if not player._phase_end then
room:drawCards(player, data.n, "game_rule")
room:drawCards(player, data.n, "phase_draw")
end
room.logic:trigger(fk.AfterDrawNCards, player, data)
end,
@ -332,8 +333,8 @@ function Phase:main()
while not player.dead do
if player._phase_end then break end
logic:trigger(fk.StartPlayCard, player, nil, true)
room:notifyMoveFocus(player, "PlayCard")
local result = room:doRequest(player, "PlayCard", player.id)
local result = Request:new(player, "PlayCard"):getResult(player)
if result == "" then break end
local useResult = room:handleUseCardReply(player, result)
@ -354,7 +355,7 @@ function Phase:main()
) - player:getMaxCards()
room:broadcastProperty(player, "MaxCards")
if discardNum > 0 then
room:askForDiscard(player, discardNum, discardNum, false, "game_rule", false)
room:askForDiscard(player, discardNum, discardNum, false, "phase_discard", false)
end
end,
[Player.Finish] = function()

View File

@ -100,29 +100,17 @@ end
---@param exchange? boolean @ 是否要替换原有判定牌(即类似鬼道那样)
function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange)
if not card then return end
local triggerResponded = self.owner_map[card:getEffectiveId()] == player
local isHandcard = (triggerResponded and self:getCardArea(card:getEffectiveId()) == Card.PlayerHand)
if triggerResponded then
local resp = {} ---@type CardResponseEvent
resp.from = player.id
resp.card = card
resp.skipDrop = true
self:responseCard(resp)
else
local move1 = {} ---@type CardsMoveInfo
move1.ids = { card:getEffectiveId() }
move1.from = player.id
move1.from = self.owner_map[card:getEffectiveId()]
move1.toArea = Card.Processing
move1.moveReason = fk.ReasonJustMove
move1.skillName = skillName
self:moveCards(move1)
end
local oldJudge = judge.card
judge.card = card
local rebyre = judge.retrial_by_response
judge.retrial_by_response = player
self:sendLog{
type = "#ChangedJudge",
@ -134,6 +122,7 @@ function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange)
Fk:filterCard(judge.card.id, judge.who, judge)
if self:getCardArea(oldJudge) == Card.Processing then
exchange = exchange and not player.dead
local move2 = {} ---@type CardsMoveInfo
@ -142,8 +131,9 @@ function JudgeEventWrappers:retrial(card, player, judge, skillName, exchange)
move2.moveReason = exchange and fk.ReasonJustMove or fk.ReasonJudge
move2.to = exchange and player.id or nil
move2.skillName = skillName
self:moveCards(move2)
end
end
return { Judge, JudgeEventWrappers }

View File

@ -397,16 +397,17 @@ end
---@param cards integer|integer[] @ 移动的牌
---@param skillName? string @ 技能名
---@param convert? boolean @ 是否可以替换装备(默认可以)
---@param proposer? ServerPlayer @ 操作者
---@param proposer? ServerPlayer | integer @ 操作者
function MoveEventWrappers:moveCardIntoEquip(target, cards, skillName, convert, proposer)
convert = (convert == nil) and true or convert
skillName = skillName or ""
cards = type(cards) == "table" and cards or {cards}
proposer = type(proposer) == "number" and self:getPlayerById(proposer) or proposer
local proposerId = proposer and proposer.id or nil
local moves = {}
for _, cardId in ipairs(cards) do
local card = Fk:getCardById(cardId)
local fromId = self.owner_map[cardId]
local proposerId = proposer and proposer.id or nil
if target:canMoveCardIntoEquip(cardId, convert) then
if target:hasEmptyEquipSlot(card.sub_type) then
table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId})

View File

@ -42,17 +42,19 @@ function Pindian:main()
table.insert(targets, pindianData.from)
pindianData.from.request_data = json.encode(data)
else
if not pindianData._fromCard then
local _pindianCard = pindianData.fromCard
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
pindianCard:addSubcard(_pindianCard.id)
pindianData.fromCard = pindianCard
pindianData._fromCard = _pindianCard
end
table.insert(moveInfos, {
ids = { _pindianCard.id },
from = room.owner_map[_pindianCard.id],
fromArea = room:getCardArea(_pindianCard.id),
ids = { pindianData._fromCard.id },
from = room.owner_map[pindianData._fromCard.id],
fromArea = room:getCardArea(pindianData._fromCard.id),
toArea = Card.Processing,
moveReason = fk.ReasonPut,
skillName = pindianData.reason,
@ -61,17 +63,19 @@ function Pindian:main()
end
for _, to in ipairs(pindianData.tos) do
if pindianData.results[to.id] and pindianData.results[to.id].toCard then
if not pindianData.results[to.id]._toCard then
local _pindianCard = pindianData.results[to.id].toCard
local pindianCard = _pindianCard:clone(_pindianCard.suit, _pindianCard.number)
pindianCard:addSubcard(_pindianCard.id)
pindianData.results[to.id].toCard = pindianCard
pindianData.results[to.id]._toCard = _pindianCard
end
table.insert(moveInfos, {
ids = { _pindianCard.id },
from = room.owner_map[_pindianCard.id],
fromArea = room:getCardArea(_pindianCard.id),
ids = { pindianData.results[to.id]._toCard.id },
from = room.owner_map[pindianData.results[to.id]._toCard.id],
fromArea = room:getCardArea(pindianData.results[to.id]._toCard.id),
toArea = Card.Processing,
moveReason = fk.ReasonPut,
skillName = pindianData.reason,
@ -79,17 +83,19 @@ function Pindian:main()
})
else
table.insert(targets, to)
to.request_data = json.encode(data)
end
end
room:notifyMoveFocus(targets, "AskForPindian")
room:doBroadcastRequest("AskForUseActiveSkill", targets)
if #targets ~= 0 then
local req = Request:new(targets, "AskForUseActiveSkill")
for _, p in ipairs(targets) do req:setData(p, data) end
req.focus_text = "AskForPindian"
for _, p in ipairs(targets) do
local _pindianCard
if p.reply_ready then
local replyCard = json.decode(p.client_reply).card
local result = req:getResult(p)
if result ~= "" then
local replyCard = result.card
_pindianCard = Fk:getCardById(json.decode(replyCard).subcards[1])
else
_pindianCard = Fk:getCardById(p:getCardIds(Player.Hand)[1])
@ -119,9 +125,10 @@ function Pindian:main()
room:sendLog{
type = "#ShowPindianCard",
from = p.id,
card = { _pindianCard.id },
arg = _pindianCard:toLogString(),
}
end
end
room:moveCards(table.unpack(moveInfos))

View File

@ -110,7 +110,7 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil
if #skill_names == 0 then return end
local losts = {} ---@type boolean[]
local triggers = {} ---@type Skill[]
local lost_piles = {} ---@type integer[]
-- local lost_piles = {} ---@type integer[]
for _, skill in ipairs(skill_names) do
if string.sub(skill, 1, 1) == "-" then
local actual_skill = string.sub(skill, 2, #skill)
@ -132,11 +132,11 @@ 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))
end
end
-- if s.derived_piles then
-- for _, pile_name in ipairs(s.derived_piles) do
-- table.insertTableIfNeed(lost_piles, player:getPile(pile_name))
-- end
-- end
end
end
else
@ -169,19 +169,26 @@ function SkillEventWrappers:handleAddLoseSkills(player, skill_names, source_skil
if (not no_trigger) and #triggers > 0 then
for i = 1, #triggers do
local event = losts[i] and fk.EventLoseSkill or fk.EventAcquireSkill
self.logic:trigger(event, player, triggers[i])
if losts[i] then
local skill = triggers[i]
skill:onLose(player)
self.logic:trigger(fk.EventLoseSkill, player, skill)
else
local skill = triggers[i]
self.logic:trigger(fk.EventAcquireSkill, player, skill)
skill:onAcquire(player)
end
end
end
if #lost_piles > 0 then
self:moveCards({
ids = lost_piles,
from = player.id,
toArea = Card.DiscardPile,
moveReason = fk.ReasonPutIntoDiscardPile,
})
end
-- if #lost_piles > 0 then
-- self:moveCards({
-- ids = lost_piles,
-- from = player.id,
-- toArea = Card.DiscardPile,
-- moveReason = fk.ReasonPutIntoDiscardPile,
-- })
-- end
end
return { SkillEffect, SkillEventWrappers }

View File

@ -858,6 +858,9 @@ function UseCardEventWrappers:handleCardEffect(event, cardEffectEvent)
extra_data = { useEventId = parentUseEvent.id, effectTo = cardEffectEvent.to }
end
end
if #players > 0 and cardEffectEvent.card.trueName == "nullification" then
self:animDelay(2)
end
local use = self:askForNullification(players, nil, nil, prompt, true, extra_data, cardEffectEvent)
if use then
use.toCard = cardEffectEvent.card

View File

@ -181,7 +181,7 @@ local function bin_search(events, from, to, n, func)
end
-- 从某个区间中找出类型符合且符合func函数检测的至多n个事件。
---@param eventType integer @ 要查找的事件类型
---@param eventType GameEvent @ 要查找的事件类型
---@param n integer @ 最多找多少个
---@param func fun(e: GameEvent): boolean? @ 过滤用的函数
---@param endEvent? GameEvent @ 区间终止点,默认为本事件结束

View File

@ -132,29 +132,18 @@ function GameLogic:chooseGenerals()
local nonlord = room:getOtherPlayers(lord, true)
local generals = table.random(room.general_pile, #nonlord * generalNum)
local req = Request:new(nonlord, "AskForGeneral")
for i, p in ipairs(nonlord) do
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
p.request_data = json.encode{ arg, n }
p.default_reply = table.random(arg, n)
req:setData(p, { arg, n })
req:setDefaultReply(p, table.random(arg, n))
end
room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord)
for _, p in ipairs(nonlord) do
local general, deputy
if p.general == "" and p.reply_ready then
local general_ret = json.decode(p.client_reply)
general = general_ret[1]
deputy = general_ret[2]
else
general = p.default_reply[1]
deputy = p.default_reply[2]
end
room:findGeneral(general)
room:findGeneral(deputy)
local result = req:getResult(p)
local general, deputy = result[1], result[2]
room:prepareGeneral(p, general, deputy)
p.default_reply = ""
end
room:askForChooseKingdom(nonlord)
@ -233,7 +222,9 @@ function GameLogic:attachSkillToPlayers()
return
end
room:handleAddLoseSkills(player, skillName, nil, false)
room:handleAddLoseSkills(player, skillName, nil, false, true)
self:trigger(fk.EventAcquireSkill, player, skill)
skill:onAcquire(player, true)
end
for _, p in ipairs(room.alive_players) do
local skills = Fk.generals[p.general].skills
@ -278,6 +269,8 @@ function GameLogic:action()
while true do
execGameEvent(GameEvent.Round)
if room.game_finished then break end
if table.every(room.players, function(p) return p.dead and p.rest == 0 end) then room:gameOver("") end
room.current = room.players[1]
end
end
@ -570,9 +563,10 @@ function GameLogic:getMostRecentEvent(eventType)
end
--- 如果当前事件刚好是技能生效事件,就返回那个技能名,否则返回空串。
---@return string|nil
function GameLogic:getCurrentSkillName()
local skillEvent = self:getCurrentEvent()
local ret = ""
local ret = nil
if skillEvent.event == GameEvent.SkillEffect then
local _, _, _skill = table.unpack(skillEvent.data)
local skill = _skill.main_skill and _skill.main_skill or _skill
@ -605,7 +599,7 @@ function GameLogic:getEventsOfScope(eventType, n, func, scope)
end
-- 在指定历史范围中找符合条件的事件(逆序)
---@param eventType integer @ 要查找的事件类型
---@param eventType GameEvent @ 要查找的事件类型
---@param func fun(e: GameEvent): boolean @ 过滤用的函数
---@param n integer @ 最多找多少个
---@param end_id integer @ 查询历史范围从最后的事件开始逆序查找直到id为end_id的事件不含

View File

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

View File

@ -1,19 +1,23 @@
---@class Request : Object
---@field public room Room
---@field public players ServerPlayer[]
---@field public n integer @ n个人做出回复后询问结束
---@field public accept_cancel? boolean @ 是否将取消也算作是收到
---@field public n integer @ 产生n个winner后询问直接结束
---@field public accept_cancel? boolean @ 是否将取消也算作是收到肯定答
---@field public ai_start_time integer? @ 只剩AI思考开始的时间微秒delay专用
---@field public timeout? integer @ 本次耗时(秒),默认为房间内配置的出手时间
---@field public command string @ command自然就是command
---@field public data table<integer, any> @ 每个player对应的询问数据
---@field public default_reply table<integer, any> @ 玩家id - 默认回复内容
---@field public send_json boolean? @ 是否需要对data使用json.encode默认true
---@field public receive_json boolean? @ 是否需要对reply使用json.decode默认true
---@field public default_reply table<integer, any> @ 玩家id - 默认回复内容 默认空串
---@field public send_encode boolean? @ 是否需要对data使用json.encode默认true
---@field public receive_decode boolean? @ 是否需要对reply使用json.decode默认true
---@field private send_success table<fk.ServerPlayer, boolean> @ 数据是否发送成功不成功的后面全部视为AI
---@field public result table<integer, any> @ 玩家id - 回复内容 nil表示完全未回复
---@field public winners ServerPlayer[] @ 按肯定回复先后顺序排序 由于有概率所有人烧条 可能会空
---@field public luck_data any? @ 是否是询问手气卡 TODO: 有需求的话把这个通用化一点
---@field private pending_requests table<fk.ServerPlayer, integer[]> @ 一控多时暂存的请求
---@field private _asked boolean? @ 是否询问过了
---@field public focus_players? ServerPlayer[] @ 要moveFocus的玩家们 默认参与者
---@field public focus_text? string @ 要moveFocus的文字 默认self.command
local Request = class("Request")
-- TODO: 懒得思考了
@ -23,9 +27,10 @@ local Request = class("Request")
-- 若还能再用一次那就重新发Request并继续等
---@param command string
---@param players ServerPlayer[]
---@param players ServerPlayer|ServerPlayer[]
---@param n? integer
function Request:initialize(command, players, n)
function Request:initialize(players, command, n)
if (not players[1]) and players.class then players = { players } end
assert(#players > 0)
self.command = command
self.players = players
@ -36,29 +41,43 @@ function Request:initialize(command, players, n)
self.room = room
self.data = {}
self.default_reply = {}
for _, p in ipairs(players) do self.default_reply[p.id] = "__cancel" end
self.timestamp = math.ceil(os.getms() / 1000)
self.timeout = room.timeout
self.send_json = true
self.receive_json = true -- 除了几个特殊字符串之外都decode
self.send_encode = true
self.receive_decode = true -- 除了几个特殊字符串之外都decode
self.pending_requests = setmetatable({}, { __mode = "k" })
self.send_success = setmetatable({}, { __mode = "k" })
self.result = {}
self.winners = {}
end
function Request:__tostring()
return "<Request>"
return string.format("<Request '%s'>", self.command)
end
---@param player ServerPlayer
---@param data any
function Request:setData(player, data)
self.data[player.id] = data
end
---@param player ServerPlayer
---@param data any @ 注意不要json.encode
function Request:setDefaultReply(player, data)
self.default_reply[player.id] = data
end
--- 获取本次Request中此人的回复若还未询问过那么先询问
--- * <any>: 成功发出回复 获取的是decode后的回复
--- * "" (空串): 发出了“取消” 或者烧完了绳子 反正就是取消
---@param player ServerPlayer
---@return any
function Request:getResult(player)
if not self._asked then self:ask() end
return self.result[player.id]
end
-- 将相应请求数据发给player
-- 不能向thinking中的玩家发送这种情况下暂存起来等待收到答复后
---@param player ServerPlayer
@ -86,7 +105,7 @@ function Request:_sendPacket(player)
-- 发送请求数据并将控制者标记为烧条中
local jsonData = self.data[player.id]
if self.send_json then jsonData = json.encode(jsonData) end
if self.send_encode then jsonData = json.encode(jsonData) end
-- FIXME: 这里确认数据是否发送的环节一定要写在C++代码中
self.send_success[controller] = controller:getState() == fk.Player_Online
controller:doRequest(self.command, jsonData, self.timeout, self.timestamp)
@ -142,34 +161,21 @@ function Request:_checkReply(player, use_ai)
if use_ai then
player.ai.command = self.command
-- FIXME: 后面进行SmartAI的时候准备爆破此处
-- player.ai.data = self.data[player.id]
player.ai.jsonData = self.data[player.id]
if player.ai:isInstanceOf(RandomAI) then
reply = "__cancel"
else
reply = player.ai:makeReply()
end
player.ai.data = self.data[player.id]
reply = Pcall(player.ai.makeReply, player.ai)
else
-- 还没轮到AI呢所以需要标记为未答复
reply = "__notready"
end
end
if reply == '' then reply = '__cancel' end
return reply
end
function Request:getWinners()
local ret = {}
for _, p in ipairs(self.players) do
local result = self.result[p.id]
if result and result ~= "" then
table.insert(ret, p)
end
end
return ret
end
function Request:ask()
if self._asked then return end
local room = self.room
-- 0. 设置计时器,防止因无人回复一直等下去
room.room:setRequestTimer(self.timeout * 1000 + 500)
@ -183,6 +189,10 @@ function Request:ask()
p.serverplayer:setThinking(false)
end
-- 发送focus
room:notifyMoveFocus(self.focus_players or self.players, self.focus_text or self.command,
math.floor(self.timeout * 1000))
-- 1. 向所有人发送询问请求
for _, p in ipairs(players) do
self:_sendPacket(p)
@ -191,10 +201,9 @@ function Request:ask()
-- 2. 进入循环等待结束条件为已有n个回复或者超时或者有人点了
-- 若很多人都取消了导致最多回复数达不到n了那么也结束
local replied_players = 0
local ready_players = 0
while true do
local changed = false
-- 判断1若投降则直接结束全部询问,若超时则踢掉所有人类玩家(这样AI还可计算
-- 若投降则直接结束全部询问,若超时则踢掉所有人类玩家(AI还可计算
if room.hasSurrendered then break end
local elapsed = os.time() - currentTime
if self.timeout - elapsed <= 0 or resume_reason == "request_timer" then
@ -205,22 +214,23 @@ function Request:ask()
end
end
-- 若players中只剩人机那么允许人机进行计算
if table.every(players, function(p)
return p.serverplayer:getState() ~= fk.Player_Online or not
self.send_success[p.serverplayer]
end) then
self.ai_start_time = os.getms()
end
-- 判断2收到足够多回复了
local use_ai = self.ai_start_time ~= nil
-- 轮询所有参与回答的玩家,如果作出了答复,那么就把他从名单移除;
-- 然后如果作出的是“肯定”答复那么添加到winner里面
for i = #players, 1, -1 do
local player = players[i]
local reply = self:_checkReply(player, use_ai)
if reply ~= "__notready" then
if reply ~= "__cancel" and self.receive_json then
if reply ~= "__cancel" and (self.receive_decode and not use_ai) then
reply = json.decode(reply)
end
self.result[player.id] = reply
@ -229,35 +239,39 @@ function Request:ask()
changed = true
if reply ~= "__cancel" or self.accept_cancel then
ready_players = ready_players + 1
if ready_players >= self.n then
for _, p in ipairs(self.players) do
table.insert(self.winners, player)
if #self.winners >= self.n then
-- winner数量已经足够剩下的人不用算了
for _, p in ipairs(players) do
-- 避免触发后续的烧条检测
if self.result[p.id] == nil then
self.result[p.id] = "__failed_in_race"
end
end
players = {} -- 清空参与者名单
break -- 注意外面还有一层循环
end
end
end
end
if #players + ready_players < self.n then break end
if ready_players >= self.n then break end
if #players == 0 then break end
if #self.winners >= self.n then break end
-- 防止万一如果AI算完后还是有机器人notready的话也别等了
-- 不然就永远别想被唤醒了
if self.ai_start_time then break end
-- 需要等待呢,等待被唤醒吧
-- 需要等待呢,等待被唤醒吧,唤醒后继续下一次轮询检测
if not changed then
resume_reason = coroutine.yield("__handleRequest")
end
end
room.room:destroyRequestTimer()
self:finish()
self:_finish()
self._asked = true
end
local function surrenderCheck(room)
@ -283,15 +297,19 @@ local function surrenderCheck(room)
end
-- 善后工作主要是result规范化、投降检测等
function Request:finish()
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也该扔了
if self.result[p.id] == "__failed_in_race" then
p:doNotify("CancelRequest", "")
self.result[p.id] = self.default_reply[p.id] or ""
end
if self.result[p.id] == nil then
self.result[p.id] = self.default_reply[p.id]
self.result[p.id] = self.default_reply[p.id] or ""
p._timewaste_count = p._timewaste_count + 1
if p._timewaste_count >= 3 and p.serverplayer:getState() == fk.Player_Online then
p._timewaste_count = 0
@ -301,10 +319,7 @@ function Request:finish()
p._timewaste_count = 0
end
if self.result[p.id] == "__cancel" then
self.result[p.id] = ""
end
if self.result[p.id] == "__failed_in_race" then
self.result[p.id] = nil
self.result[p.id] = (not self.accept_cancel) and self.default_reply[p.id] or ""
end
end
room.last_request = self
@ -312,7 +327,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

View File

@ -15,18 +15,20 @@
---@field public game_finished boolean @ 游戏是否已经结束
---@field public tag table<string, any> @ Tag清单其实跟Player的标记是差不多的东西
---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组
---@field public disabled_packs string[] @ 未开启的扩展包名(是小包名,不是大包名)
---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动
---@field public request_queue table<userdata, table>
---@field public request_self table<integer, integer>
---@field public last_request Request @ 上一次完成的request
---@field public skill_costs table<string, any> @ 存放skill.cost_data用
---@field public card_marks table<integer, any> @ 存放card.mark之用
---@field public current_cost_skill TriggerSkill? @ AI用
local Room = AbstractRoom:subclass("Room")
-- load classes used by the game
Request = require "server.network"
GameEvent = require "server.gameevent"
local GameEventWrappers = require "lua/server/events"
GameEventWrappers = require "lua/server/events"
Room:include(GameEventWrappers)
GameLogic = require "server.gamelogic"
ServerPlayer = require "server.serverplayer"
@ -508,10 +510,11 @@ function Room:setDeputyGeneral(player, general)
self:notifyProperty(player, player, "deputyGeneral")
end
--- 为角色设置武将,并从武将池中抽出,若有隐匿技变为隐匿将。注意此时不会进行选择势力,请随后自行处理
---@param player ServerPlayer
---@param general string
---@param deputy string
---@param broadcast boolean|nil
---@param general string @ 主将名
---@param deputy? string @ 副将名
---@param broadcast? boolean @ 是否公示,默认否
function Room:prepareGeneral(player, general, deputy, broadcast)
self:findGeneral(general)
self:findGeneral(deputy)
@ -597,9 +600,9 @@ end
function Room:doRequest(player, command, jsonData, wait)
-- fk.qCritical("Room:doRequest is deprecated!")
if wait == true then error("wait can't be true") end
local request = Request:new(command, {player})
request.send_json = false -- 因为参数已经json.encode过了该死的兼容性
request.receive_json = false
local request = Request:new(player, command)
request.send_encode = false -- 因为参数已经json.encode过了该死的兼容性
request.receive_decode = false
request.accept_cancel = true
request:setData(player, jsonData)
request:ask()
@ -613,9 +616,9 @@ end
function Room:doBroadcastRequest(command, players, jsonData)
-- fk.qCritical("Room:doBroadcastRequest is deprecated!")
players = players or self.players
local request = Request:new(command, players)
request.send_json = false -- 因为参数已经json.encode过了
request.receive_json = false
local request = Request:new(players, command)
request.send_encode = false -- 因为参数已经json.encode过了
request.receive_decode = false
request.accept_cancel = true
for _, p in ipairs(players) do
request:setData(p, jsonData or p.request_data)
@ -635,14 +638,14 @@ end
function Room:doRaceRequest(command, players, jsonData)
-- fk.qCritical("Room:doRaceRequest is deprecated!")
players = players or self.players
local request = Request:new(command, players, 1)
request.send_json = false -- 因为参数已经json.encode过了
request.receive_json = false
local request = Request:new(players, command, 1)
request.send_encode = false -- 因为参数已经json.encode过了
request.receive_decode = false
for _, p in ipairs(players) do
request:setData(p, jsonData or p.request_data)
end
request:ask()
return request:getWinners()[1]
return request.winners[1]
end
--- 延迟一段时间。
@ -656,6 +659,15 @@ function Room:delay(ms)
coroutine.yield("__handleRequest", ms)
end
--- 延迟一段时间。界面上会显示所有人读条了。注意这个只能延迟多少秒。
---@param sec integer @ 要延迟的秒数
function Room:animDelay(sec)
local req = Request:new(self.alive_players, "EmptyRequest")
req.focus_text = ''
req.timeout = sec
req:ask()
end
--- 向多名玩家告知一次移牌行为。
---@param players? ServerPlayer[] @ 要被告知的玩家列表,默认为全员
---@param card_moves CardsMoveStruct[] @ 要告知的移牌信息列表
@ -691,7 +703,8 @@ end
--- 形象点说,就是在那些玩家下面显示一个“弃牌 思考中...”之类的烧条提示。
---@param players ServerPlayer | ServerPlayer[] @ 要获得焦点的一名或者多名角色
---@param command string @ 烧条的提示文字
function Room:notifyMoveFocus(players, command)
---@param timeout integer? @ focus的烧条时长
function Room:notifyMoveFocus(players, command, timeout)
if (players.class) then
players = {players}
end
@ -712,7 +725,8 @@ function Room:notifyMoveFocus(players, command)
self:doBroadcastNotify("MoveFocus", json.encode{
ids,
command
command,
timeout
})
end
@ -896,18 +910,20 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra
end
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, extra_data}
Fk.currentResponseReason = extra_data.skillName
local result = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req:setData(player, data)
req.focus_text = extra_data.skillName or skill_name
local result = req:getResult(player)
Fk.currentResponseReason = nil
if result == "" then
return false
end
data = json.decode(result)
data = result
local card = data.card
local targets = data.targets
local card_data = json.decode(card)
@ -969,7 +985,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can
return false
end
end
if skillName == "game_rule" then
if skillName == "phase_discard" then
status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
if skill:excludeFrom(player, card) then
@ -1213,7 +1229,7 @@ end
---@param minNum? integer @ 最少交出的卡牌数默认0
---@param maxNum? integer @ 最多交出的卡牌数,默认所有牌
---@param prompt? string @ 询问提示信息
---@param expand_pile? string @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写
---@param expand_pile? string|integer[] @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写
---@param skipMove? boolean @ 是否跳过移动。默认不跳过
---@param single_max? integer|table @ 限制每人能获得的最大牌数。输入整数或(以角色id为键以整数为值)的表
---@return table<integer, integer[]> @ 返回一个表键为角色id转字符串值为分配给其的牌id数组
@ -1346,7 +1362,7 @@ function Room:returnToGeneralPile(g, position)
end
--- 抽特定名字的武将(抽了就没了)
---@param name string @ 武将name如找不到则查找truename再找不到则返回nil
---@param name string? @ 武将name如找不到则查找truename再找不到则返回nil
---@return string? @ 抽出的武将名
function Room:findGeneral(name)
if not Fk.generals[name] then return nil end
@ -1384,27 +1400,20 @@ end
---@return string|string[] @ 选择的武将
function Room:askForGeneral(player, generals, n, noConvert)
local command = "AskForGeneral"
self:notifyMoveFocus(player, command)
n = n or 1
if #generals == n then return n == 1 and generals[1] or generals end
local defaultChoice = table.random(generals, n)
if (player.serverplayer:getState() == fk.Player_Online) then
local result = self:doRequest(player, command, json.encode{ generals, n, noConvert })
local choices
if result == "" then
choices = defaultChoice
else
choices = json.decode(result)
end
local req = Request:new(player, command)
local data = { generals, n, noConvert }
req:setData(player, data)
req:setDefaultReply(player, defaultChoice)
local choices = req:getResult(player)
if #choices == 1 then return choices[1] end
return choices
end
return n == 1 and defaultChoice[1] or defaultChoice
end
--- 询问玩家若为神将、双势力需选择一个势力。
---@param players? ServerPlayer[] @ 询问目标
function Room:askForChooseKingdom(players)
@ -1414,7 +1423,9 @@ function Room:askForChooseKingdom(players)
end)
if #specialKingdomPlayers > 0 then
local choiceMap = {}
local req = Request:new(specialKingdomPlayers, "AskForChoice")
req.focus_text = "AskForKingdom"
req.receive_decode = false
for _, p in ipairs(specialKingdomPlayers) do
local allKingdoms = {}
local curGeneral = Fk.generals[p.general]
@ -1424,25 +1435,13 @@ function Room:askForChooseKingdom(players)
allKingdoms = Fk:getKingdomMap(p.kingdom)
end
if #allKingdoms > 0 then
choiceMap[p.id] = allKingdoms
local data = json.encode({ allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" })
p.request_data = data
req:setData(p, { allKingdoms, allKingdoms, "AskForKingdom", "#ChooseInitialKingdom" })
req:setDefaultReply(p, allKingdoms[1])
end
end
self:notifyMoveFocus(players, "AskForKingdom")
self:doBroadcastRequest("AskForChoice", specialKingdomPlayers)
for _, p in ipairs(specialKingdomPlayers) do
local kingdomChosen
if p.reply_ready then
kingdomChosen = p.client_reply
else
kingdomChosen = choiceMap[p.id][1]
end
p.kingdom = kingdomChosen
p.kingdom = req:getResult(p)
self:notifyProperty(p, p, "kingdom")
end
end
@ -1458,9 +1457,10 @@ end
function Room:askForCardChosen(chooser, target, flag, reason, prompt)
local command = "AskForCardChosen"
prompt = prompt or ""
self:notifyMoveFocus(chooser, command)
local data = {target.id, flag, reason, prompt}
local result = self:doRequest(chooser, command, json.encode(data))
local req = Request:new(chooser, command)
req:setData(chooser, data)
local result = req:getResult(chooser)
if result == "" then
local areas = {}
@ -1478,8 +1478,6 @@ function Room:askForCardChosen(chooser, target, flag, reason, prompt)
end
if #handcards == 0 then return end
result = handcards[math.random(1, #handcards)]
else
result = tonumber(result)
end
if result == -1 then
@ -1507,18 +1505,20 @@ function Room:askForPoxi(player, poxi_type, data, extra_data, cancelable)
if not poxi then return {} end
local command = "AskForPoxi"
self:notifyMoveFocus(player, poxi_type)
local result = self:doRequest(player, command, json.encode {
local req = Request:new(player, command)
req.focus_text = poxi_type
req:setData(player, {
type = poxi_type,
data = data,
extra_data = extra_data,
cancelable = (cancelable == nil) and true or cancelable
})
local result = req:getResult(player)
if result == "" then
return poxi.default_choice(data, extra_data)
else
return poxi.post_select(json.decode(result), data, extra_data)
return poxi.post_select(result, data, extra_data)
end
end
@ -1535,7 +1535,7 @@ end
---@return integer[] @ 选择的id
function Room:askForCardsChosen(chooser, target, min, max, flag, reason, prompt)
if min == 1 and max == 1 then
return { self:askForCardChosen(chooser, target, flag, reason) }
return { self:askForCardChosen(chooser, target, flag, reason, prompt) }
end
local cards
@ -1607,10 +1607,15 @@ function Room:askForChoice(player, choices, skill_name, prompt, detailed, all_ch
local command = "AskForChoice"
prompt = prompt or ""
all_choices = all_choices or choices
self:notifyMoveFocus(player, skill_name)
local result = self:doRequest(player, command, json.encode{
local req = Request:new(player, command)
req.focus_text = skill_name
req.receive_decode = false -- 这个不用decode
req:setData(player, {
choices, all_choices, skill_name, prompt, detailed
})
local result = req:getResult(player)
if result == "" then
if table.contains(choices, "Cancel") then
result = "Cancel"
@ -1637,15 +1642,19 @@ function Room:askForChoices(player, choices, minNum, maxNum, skill_name, prompt,
if #choices <= minNum and not all_choices and not cancelable then return choices end
assert(minNum <= maxNum)
assert(not all_choices or table.every(choices, function(c) return table.contains(all_choices, c) end))
local command = "AskForChoices"
skill_name = skill_name or ""
prompt = prompt or ""
all_choices = all_choices or choices
detailed = detailed or false
self:notifyMoveFocus(player, skill_name)
local result = self:doRequest(player, command, json.encode{
local req = Request:new(player, command)
req.focus_text = skill_name
req:setData(player, {
choices, all_choices, {minNum, maxNum}, cancelable, skill_name, prompt, detailed
})
local result = req:getResult(player)
if result == "" then
if cancelable then
return {}
@ -1653,7 +1662,7 @@ function Room:askForChoices(player, choices, minNum, maxNum, skill_name, prompt,
return table.random(choices, math.min(minNum, #choices))
end
end
return json.decode(result)
return result
end
--- 询问玩家是否发动技能。
@ -1664,11 +1673,11 @@ end
---@return boolean
function Room:askForSkillInvoke(player, skill_name, data, prompt)
local command = "AskForSkillInvoke"
self:notifyMoveFocus(player, skill_name)
local invoked = false
local result = self:doRequest(player, command, json.encode{ skill_name, prompt })
if result ~= "" then invoked = true end
return invoked
local req = Request:new(player, command)
req.focus_text = skill_name
req.receive_decode = false -- 这个返回的都是"1" 不用decode
req:setData(player, { skill_name, prompt })
return req:getResult(player) ~= ""
end
-- 获取使用牌的合法额外目标(【借刀杀人】等带副目标的卡牌除外)
@ -1795,7 +1804,9 @@ function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrang
poxi_type = poxi_type or "",
cancelable = ((pattern ~= "." or poxi_type ~= "") and (default_choice == nil))
}
local result = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req:setData(player, data)
local result = req:getResult(player)
-- local result = player.room:askForCustomDialog(player, skillname,
-- "RoomElement/ArrangeCardsBox.qml", {
-- cardMap, prompt, box_size, max_limit, min_limit, free_arrange or false, areaNames,
@ -1829,7 +1840,7 @@ function Room:askForArrangeCards(player, skillname, cardMap, prompt, free_arrang
end
return cardMap
end
return json.decode(result)
return result
end
-- TODO: guanxing type
@ -1867,7 +1878,6 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
areaNames = { "Top", "Bottom" }
end
local command = "AskForGuanxing"
self:notifyMoveFocus(player, customNotify or command)
local max_top = top_limit[2]
local card_map = {}
if max_top > 0 then
@ -1888,10 +1898,13 @@ function Room:askForGuanxing(player, cards, top_limit, bottom_limit, customNotif
bottom_area_name = areaNames[2],
}
local result = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req.focus_text = customNotify
req:setData(player, data)
local result = req:getResult(player)
local top, bottom
if result ~= "" then
local d = json.decode(result)
local d = result
if top_limit[2] == 0 then
top = Util.DummyTable
bottom = d[1]
@ -1942,15 +1955,17 @@ function Room:askForExchange(player, piles, piles_name, customNotify)
elseif x < 0 then
piles_name = table.slice(piles_name, 1, #piles + 1)
end
self:notifyMoveFocus(player, customNotify or command)
local data = {
piles = piles,
piles_name = piles_name,
}
local result = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req.focus_text = customNotify
req:setData(player, data)
local result = req:getResult(player)
if result ~= "" then
local d = json.decode(result)
return d
return result
else
return piles
end
@ -1960,7 +1975,7 @@ end
---@param data string
---@return CardUseStruct
function Room:handleUseCardReply(player, data)
data = json.decode(data)
-- data = json.decode(data)
local card = data.card
local targets = data.targets
if type(card) == "string" then
@ -2062,7 +2077,6 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr
end
local command = "AskForUseCard"
self:notifyMoveFocus(player, card_name)
cancelable = (cancelable == nil) and true or cancelable
extra_data = extra_data or Util.DummyTable
prompt = prompt or ""
@ -2088,7 +2102,12 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr
Fk.currentResponsePattern = pattern
self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true)
local result = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req.focus_text = card_name or ""
req:setData(player, data)
local result = req:getResult(player)
askForUseCardData.afterRequest = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, askForUseCardData, true)
Fk.currentResponsePattern = nil
@ -2124,7 +2143,6 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext
end
local command = "AskForResponseCard"
self:notifyMoveFocus(player, card_name)
cancelable = (cancelable == nil) and true or cancelable
extra_data = extra_data or Util.DummyTable
pattern = pattern or card_name
@ -2152,7 +2170,12 @@ function Room:askForResponse(player, card_name, pattern, prompt, cancelable, ext
Fk.currentResponsePattern = pattern
eventData.isResponse = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
local result = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req.focus_text = card_name or ""
req:setData(player, data)
local result = req:getResult(player)
eventData.afterRequest = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
Fk.currentResponsePattern = nil
@ -2206,8 +2229,6 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl
repeat
useResult = nil
self:notifyMoveFocus(self.alive_players, card_name)
self:doBroadcastNotify("WaitForNullification", "")
local data = {card_name, pattern, prompt, cancelable, extra_data, disabledSkillNames}
@ -2220,12 +2241,19 @@ function Room:askForNullification(players, card_name, pattern, prompt, cancelabl
eventData = effectData,
}
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
local winner = self:doRaceRequest(command, players, json.encode(data))
local req = Request:new(players, command, 1)
req.focus_players = self.alive_players
req.focus_text = card_name
for _, p in ipairs(players) do req:setData(p, data) end
req:ask()
local winner = req.winners[1]
eventData.afterRequest = true
self.logic:trigger(fk.HandleAskForPlayCard, nil, eventData, true)
if winner then
local result = winner.client_reply
local result = req:getResult(winner)
useResult = self:handleUseCardReply(winner, result)
if type(useResult) == "string" and useResult ~= "" then
@ -2259,13 +2287,17 @@ function Room:askForAG(player, id_list, cancelable, reason)
end
local command = "AskForAG"
self:notifyMoveFocus(player, reason or command)
local data = { id_list, cancelable, reason }
local ret = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req.focus_text = reason
req:setData(player, data)
local ret = req:getResult(player)
if ret == "" and not cancelable then
ret = table.random(id_list)
end
return tonumber(ret)
return ret
end
--- 给player发一条消息在他的窗口中用一系列卡牌填充一个AG。
@ -2356,22 +2388,22 @@ function Room:askForMiniGame(players, focus, game_type, data_table)
local command = "MiniGame"
local game = Fk.mini_games[game_type]
if #players == 0 or not game then return end
local req = Request:new(players, command)
req.focus_text = focus
req.receive_decode = false -- 和customDialog同理
for _, p in ipairs(players) do
local data = data_table[p.id]
p.mini_game_data = { type = game_type, data = data }
p.request_data = json.encode(p.mini_game_data)
p.default_reply = game.default_choice and json.encode(game.default_choice(p, data)) or ""
req:setData(p, p.mini_game_data)
req:setDefaultReply(p, game.default_choice and json.encode(game.default_choice(p, data)))
end
self:notifyMoveFocus(players, focus)
self:doBroadcastRequest(command, players)
req:ask()
for _, p in ipairs(players) do
p.mini_game_data = nil
if not p.reply_ready then
p.client_reply = p.default_reply
p.reply_ready = true
end
end
end
@ -2386,11 +2418,14 @@ end
---@return string
function Room:askForCustomDialog(player, focustxt, qmlPath, extra_data)
local command = "CustomDialog"
self:notifyMoveFocus(player, focustxt)
return self:doRequest(player, command, json.encode{
local req = Request:new(player, command)
req.focus_text = focustxt
req.receive_decode = false -- 没法知道要不要decode所以我写false (json.decode该杀啊)
req:setData(player, {
path = qmlPath,
data = extra_data,
})
return req:getResult(player)
end
--- 询问移动场上的一张牌
@ -2475,14 +2510,13 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla
playerIds = { targetOne.id, targetTwo.id }
}
local command = "AskForMoveCardInBoard"
self:notifyMoveFocus(player, command)
local result = self:doRequest(player, command, json.encode(data))
local req = Request:new(player, command)
req:setData(player, data)
local result = req:getResult(player)
if result == "" then
local randomIndex = math.random(1, #cards)
result = { cardId = cards[randomIndex], pos = cardsPosition[randomIndex] }
else
result = json.decode(result)
end
local from, to
@ -2942,10 +2976,5 @@ function Room:removeTableMark(sth, mark, value)
end
end
function Room:__index(k)
if k == "room_settings" then
return self.settings
end
end
return Room

View File

@ -1,6 +1,6 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
local Room = require "server.room"
Room = require "server.room"
-- 所有当前正在运行的房间(即游戏尚未结束的房间)
---@type table<integer, Room>

View File

@ -30,13 +30,6 @@ function ServerPlayer:initialize(_self)
self.id = _self:getId()
self.room = nil
-- Below are for doBroadcastRequest
-- 但是几乎全部被船新request杀了
self.request_data = ""
--self.client_reply = ""
self.default_reply = ""
--self.reply_ready = false
--self.reply_cancel = false
self.phases = {}
self.skipped_phases = {}
self.phase_state = {}
@ -47,7 +40,7 @@ function ServerPlayer:initialize(_self)
self._prelighted_skills = {}
self._timewaste_count = 0
self.ai = RandomAI:new(self)
self.ai = SmartAI:new(self)
end
---@param command string
@ -284,6 +277,9 @@ end
function ServerPlayer:play(phase_table)
phase_table = phase_table or {}
if #phase_table > 0 then
if not table.contains(phase_table, Player.RoundStart) then
table.insert(phase_table, 1, Player.RoundStart)
end
if not table.contains(phase_table, Player.NotActive) then
table.insert(phase_table, Player.NotActive)
end
@ -384,17 +380,19 @@ function ServerPlayer:endCurrentPhase()
end
--- 获得一个额外回合
---@param delay? boolean
---@param skillName? string
function ServerPlayer:gainAnExtraTurn(delay, skillName)
---@param delay? boolean @ 是否延迟到当前回合结束再开启额外回合,默认是
---@param skillName? string @ 额外回合原因
---@param turnData? TurnStruct @ 额外回合的信息
function ServerPlayer:gainAnExtraTurn(delay, skillName, turnData)
local room = self.room
delay = (delay == nil) and true or delay
skillName = (skillName == nil) and room.logic:getCurrentSkillName() or skillName
skillName = skillName or room.logic:getCurrentSkillName() or "game_rule"
turnData = turnData or {}
turnData.reason = skillName
if delay then
local logic = room.logic
local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true)
local turn = room.logic:getCurrentEvent():findParent(GameEvent.Turn, true)
if turn then
turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName) end)
turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName, turnData) end)
return
end
end
@ -407,13 +405,15 @@ function ServerPlayer:gainAnExtraTurn(delay, skillName)
local current = room.current
room.current = self
self.tag["_extra_turn_count"] = self.tag["_extra_turn_count"] or {}
local ex_tag = self.tag["_extra_turn_count"]
table.insert(ex_tag, skillName)
room:addTableMark(self, "_extra_turn_count", skillName)
GameEvent.Turn:create(self):exec()
GameEvent.Turn:create(self, turnData):exec()
table.remove(ex_tag)
local mark = self:getTableMark("_extra_turn_count")
if #mark > 0 then
table.remove(mark)
room:setPlayerMark(self, "_extra_turn_count", mark)
end
room.current = current
end
@ -421,17 +421,14 @@ end
--- 当前是否处于额外的回合。
--- @return boolean
function ServerPlayer:insideExtraTurn()
return self.tag["_extra_turn_count"] and #self.tag["_extra_turn_count"] > 0
return self:getCurrentExtraTurnReason() ~= "game_rule"
end
--- 当前额外回合的技能原因。
--- 当前额外回合的技能原因。非额外回合则为game_rule
---@return string
function ServerPlayer:getCurrentExtraTurnReason()
local ex_tag = self.tag["_extra_turn_count"]
if (not ex_tag) or #ex_tag == 0 then
return "game_rule"
end
return ex_tag[#ex_tag]
local mark = self:getTableMark("_extra_turn_count")
return mark[#mark] or "game_rule"
end
--- 角色摸牌。
@ -461,6 +458,7 @@ function ServerPlayer:bury()
self:throwAllCards()
self:throwAllMarks()
self:clearPiles()
self:onAllSkillLose()
self:reset()
end
@ -482,6 +480,12 @@ function ServerPlayer:throwAllCards(flag)
self.room:throwCard(cardIds, "", self)
end
function ServerPlayer:onAllSkillLose()
for _, skill in ipairs(self:getAllSkills()) do
skill:onLose(self, true)
end
end
function ServerPlayer:throwAllMarks()
for name, _ in pairs(self.mark) do
self.room:setPlayerMark(self, name, 0)

View File

@ -246,6 +246,11 @@ fk.IceDamage = 4
---@field public skillName string @ 技能名
---@field public fromPlace "top"|"bottom" @ 摸牌的位置
---@--- TurnStruct 回合事件的数据
---@class TurnStruct
---@field public reason string? @ 当前额外回合的原因不为额外回合则为game_rule
---@field public phase_table? Phase[] @ 此回合将进行的阶段,填空则为正常流程
--- 移动理由
---@alias CardMoveReason integer
fk.ReasonJustMove = 1

View File

@ -4,6 +4,11 @@ local SelectableItem = base.SelectableItem
---@class CardItem: SelectableItem
local CardItem = SelectableItem:subclass("CardItem")
function CardItem:initialize(scene, id)
SelectableItem.initialize(self, scene, id)
Fk:filterCard(id, Fk:currentRoom():getCardOwner(id))
end
---@class Photo: SelectableItem
---@field public state string
local Photo = SelectableItem:subclass("Photo")

View File

@ -1,180 +1,10 @@
--[[
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 -- 使用优先值
}
local slash = fk.ai_skills["slash_skill"]
local just_use = fk.ai_skills["__just_use"]
local use_to_enemy = fk.ai_skills["__use_to_enemy"]
SmartAI:setSkillAI("thunder__slash_skill", slash)
SmartAI:setSkillAI("fire__slash_skill", slash)
SmartAI:setSkillAI("analeptic_skill", just_use)
SmartAI:setSkillAI("iron_chain_skill", just_use)
SmartAI:setSkillAI("fire_attack_skill", use_to_enemy)
SmartAI:setSkillAI("supply_shortage_skill", use_to_enemy)
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
--]]

View File

@ -1,55 +1,11 @@
-- aux_skill的AI文件。aux_skill的重量级程度无需多说。
-- 这个文件说是第二个smart_ai.lua也不为过。
SmartAI:setSkillAI("discard_skill", {
choose_targets = function(_, ai)
return ai:doOKButton()
end,
})
-- discard_skill: 弃牌相关AI
-----------------------------
--- 弃牌相关判定函数的表。键为技能名,值为原型如下的函数。
---@type table<string, fun(self: SmartAI, min_num: number, num: number, include_equip?: boolean, cancelable?: boolean, pattern: string, prompt: string): integer[]?>
fk.ai_discard = {}
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
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
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
---@field targets integer[]
--- 选人相关判定函数的表。键为技能名,值为原型如下的函数。
---@type table<string, fun(self: SmartAI, targets: integer[], min_num: number, num: number, cancelable?: boolean): ChoosePlayersReply?>
fk.ai_choose_players = {}
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
SmartAI:setSkillAI("choose_cards_skill", {
choose_targets = function(_, ai)
return ai:doOKButton()
end,
})

View File

@ -1,80 +1,186 @@
SmartAI:setSkillAI("ganglie", {
think = function(self, ai)
-- 刚烈的think中要处理两种情况一是askForSkillInvoke的确定取消二是被刚烈的人决定是否弃置2牌
if ai:getPrompt():startsWith("#AskForDiscard") then
-- 权衡一下弃牌与扣血的收益
-- local cancel_val = 模拟自己扣血的收益
-- local ok_val = 模拟弃两张最垃圾牌的收益
-- 比如说等于discard_skill_ai:think()的收益什么的
-- if ok_val > cancel_val then
-- return ai:doOKButton()
-- else
-- return ""
-- end
else
-- 模拟一下self.skill:use 计算收益是否为正
return false
end
end,
})
SmartAI:setTriggerSkillAI("dawu", {
correct_func = function(self, logic, event, target, player, data)
if event ~= fk.DamageInflicted then return end
return self.skill:triggerable(event, target, player, data)
end,
})
--[=[
if UsingNewCore then
require "standard.ai.aux_skills"
else
require "packages.standard.ai.aux_skills"
end
local true_invoke = { skill_invoke = true }
local enemy_damage_invoke = {
skill_invoke = function(skill, ai)
local room = ai.room
local logic = room.logic
local event = logic:getCurrentEvent()
local dmg = event.data[1]
return ai:isEnemy(dmg.from)
end
}
---@type SmartAISkillSpec
local active_random_select_card = {
will_use = Util.TrueFunc,
---@param skill ViewAsSkill
choose_cards = function(skill, ai)
repeat
local cids = ai:getEnabledCards()
if #cids == 0 then return ai:okButtonEnabled() end
ai:selectCard(cids[1], true)
until ai:okButtonEnabled() or ai:hasEnabledTarget()
return true
end,
}
local use_to_enemy = fk.ai_skills["__use_to_enemy"]
local use_to_friend = fk.ai_skills["__use_to_friend"]
local just_use = fk.ai_skills["__just_use"]
-- 魏国
fk.ai_skill_invoke["jianxiong"] = true
SmartAI:setSkillAI("jianxiong", true_invoke)
-- TODO: hujia
-- TODO: guicai 关于如何界定判定的好坏 需要向AI中单独说明
fk.ai_skill_invoke["fankui"] = function(self)
local room = self.room
-- TODO: guicai 关于如何界定判定的好坏 需要向AI中单独说明
SmartAI:setSkillAI("fankui", enemy_damage_invoke)
SmartAI:setSkillAI("ganglie", {
skill_invoke = function(skill, ai)
local room = ai.room
local logic = room.logic
-- 询问反馈时处于on_cost环节当前事件必是damage且有from
local event = logic:getCurrentEvent()
local dmg = event.data[1]
return self:isEnemy(dmg.from)
return ai:isEnemy(dmg.from)
end,
choose_cards = function(skill, ai)
local cards = ai:getEnabledCards()
if #cards > 2 then
for i = 1, 2 do ai:selectCard(cards[i], true) end
return true
end
return false -- 直接按取消键
end,
-- choose_targets只有个按ok 复用默认
})
fk.ai_skill_invoke["ganglie"] = fk.ai_skill_invoke["fankui"]
-- TODO: tuxi
fk.ai_skill_invoke["luoyi"] = function(self)
return false
SmartAI:setSkillAI("tuxi", {
choose_targets = function(skill, ai)
local targets = ai:getEnabledTargets()
local i = 0
for _, p in ipairs(targets) do
if ai:isEnemy(p) then
ai:selectTarget(p, true)
i = i + 1
if i >= 2 then return ai:doOKButton() end
end
end
end
})
fk.ai_skill_invoke["tiandu"] = true
SmartAI:setSkillAI("luoyi", { skill_invoke = false })
-- TODO: yiji
SmartAI:setSkillAI("tiandu", true_invoke)
SmartAI:setSkillAI("yiji", {
skill_invoke = true,
-- ask_active = function
})
fk.ai_skill_invoke["luoshen"] = true
-- TODO: qingguo
SmartAI:setSkillAI("luoshen", true_invoke)
SmartAI:setSkillAI("qingguo", active_random_select_card)
-- 蜀国
-- TODO: rende
-- TODO: jijiang
-- TODO: wusheng
-- TODO: guanxing
-- TODO: longdan
SmartAI:setSkillAI("rende", active_random_select_card)
SmartAI:setSkillAI("rende", use_to_friend)
fk.ai_skill_invoke["tieqi"] = function(self)
local room = self.room
-- TODO: jijiang
SmartAI:setSkillAI("wusheng", active_random_select_card)
-- TODO: guanxing
-- TODO: longdan
SmartAI:setSkillAI("longdan", active_random_select_card)
SmartAI:setSkillAI("tieqi", {
skill_invoke = function(skill, ai)
local room = ai.room
local logic = room.logic
-- 询问反馈时处于on_cost环节当前事件必是damage且有from
local event = logic:getCurrentEvent()
local use = event.data[1] ---@type CardUseStruct
return table.find(use.tos, function(t)
return self:isEnemy(room:getPlayerById(t[1]))
return ai:isEnemy(room:getPlayerById(t[1]))
end)
end
})
fk.ai_skill_invoke["jizhi"] = true
SmartAI:setSkillAI("jizhi", true_invoke)
-- 吴国
-- TODO: zhiheng
SmartAI:setSkillAI("zhiheng", {
choose_cards = function(self, ai)
for _, cid in ipairs(ai:getEnabledCards()) do
ai:selectCard(cid, true)
end
return true
end,
})
SmartAI:setSkillAI("zhiheng", just_use)
-- TODO: qixi
SmartAI:setSkillAI("qixi", active_random_select_card)
fk.ai_skill_invoke["keji"] = true
SmartAI:setSkillAI("keji", true_invoke)
-- TODO: kurou
SmartAI:setSkillAI("kurou", just_use)
fk.ai_skill_invoke["yingzi"] = true
SmartAI:setSkillAI("yingzi", true_invoke)
-- TODO: fanjian
-- TODO: guose
SmartAI:setSkillAI("fanjian", use_to_enemy)
SmartAI:setSkillAI("guose", active_random_select_card)
-- TODO: liuli
fk.ai_skill_invoke["lianying"] = true
fk.ai_skill_invoke["xiaoji"] = true
SmartAI:setSkillAI("lianying", true_invoke)
-- TODO: jieyin
SmartAI:setSkillAI("xiaoji", true_invoke)
SmartAI:setSkillAI("jieyin", active_random_select_card)
SmartAI:setSkillAI("jieyin", use_to_friend)
-- 群雄
-- TODO: qingnang
SmartAI:setSkillAI("qingnang", active_random_select_card)
SmartAI:setSkillAI("qingnang", use_to_friend)
-- TODO: jijiu
-- TODO: wushuang
SmartAI:setSkillAI("qingnang", active_random_select_card)
-- TODO: lijian
fk.ai_skill_invoke["biyue"] = true
SmartAI:setSkillAI("biyue", true_invoke)
--]=]

View File

@ -27,7 +27,7 @@ local discardSkill = fk.CreateActiveSkill{
return false
end
end
if Fk.currentResponseReason == "game_rule" then
if Fk.currentResponseReason == "phase_discard" then
status_skills = Fk:currentRoom().status_skills[MaxCardsSkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
if skill:excludeFrom(Self, card) then

View File

@ -1,14 +1,5 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---@param killer ServerPlayer
local function rewardAndPunish(killer, victim)
if killer.dead then return end
if victim.role == "rebel" then
killer:drawCards(3, "kill")
elseif victim.role == "loyalist" and killer.role == "lord" then
killer:throwAllCards("he")
end
end
GameRule = fk.CreateTriggerSkill{
name = "game_rule",
@ -90,14 +81,11 @@ GameRule = fk.CreateTriggerSkill{
end,
[fk.BuryVictim] = function()
player:bury()
if room.tag["SkipNormalDeathProcess"] or player.rest > 0 then
if room.tag["SkipNormalDeathProcess"] or player.rest > 0 or (data.extra_data and data.extra_data.skip_reward_punish) then
return false
end
local damage = data.damage
if damage and damage.from then
local killer = damage.from
rewardAndPunish(killer, player);
end
Fk.game_modes[room.settings.gameMode]:deathRewardAndPunish(player, damage and damage.from)
end,
default = function()
print("game_rule: Event=" .. event)

View File

@ -222,7 +222,7 @@ Fk:loadTranslationTable({
["revealMain"] = "Reveal main character %arg",
["revealDeputy"] = "Reveal deputy character %arg",
["game_rule"] = "Discard",
["game_rule"] = "GameRule",
}, "en_US")
-- init

View File

@ -298,6 +298,8 @@ Fk:loadTranslationTable{
["lijian"] = "离间",
[":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。",
["#lijian-active"] = "发动 离间,弃置一张手牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】",
["lijian_tip_1"] = "先出杀",
["lijian_tip_2"] = "后出杀",
["$biyue1"] = "失礼了~",
["$biyue2"] = "羡慕吧~",
["biyue"] = "闭月",
@ -529,13 +531,23 @@ Fk:loadTranslationTable{
["ex__choose_skill"] = "选择",
["distribution_select_skill"] = "分配",
["choose_players_to_move_card_in_board"] = "选择角色",
["AskForUseActiveSkill"] = "使用技能",
["AskForSkillInvoke"] = "发动技能",
["AskForUseCard"] = "使用",
["AskForResponseCard"] = "打出",
["AskForDiscard"] = "弃牌",
["AskForCardChosen"] = "选牌",
["AskForCardsChosen"] = "选牌",
["AskForPindian"] = "拼点",
["reveal_skill&"] = "亮将",
["#reveal_skill&"] = "选择一个武将亮将(点击左侧选择框展开)",
[":reveal_skill&"] = "出牌阶段,你可明置一张有锁定技的武将。",
["revealMain"] = "明置主将 %arg",
["revealDeputy"] = "明置副将 %arg",
["game_rule"] = "弃牌阶段",
["game_rule"] = "游戏规则",
["replace_equip"] = "替换装备",
["#EquipmentChoice"] = "%arg",
["#GameRuleReplaceEquipment"] = "请选择要置入的区域",

View File

@ -1097,6 +1097,14 @@ local lijian = fk.CreateActiveSkill{
}
room:useCard(new_use)
end,
target_tip = function(self, to_select, selected, _, __, selectable, ____)
if not selectable then return end
if #selected == 0 or (#selected > 0 and selected[1] == to_select) then
return "lijian_tip_1"
else
return "lijian_tip_2"
end
end,
}
local biyue = fk.CreateTriggerSkill{
name = "biyue",
@ -1215,30 +1223,20 @@ local role_getlogic = function()
end
local nonlord = room:getOtherPlayers(lord, true)
local req = Request:new(nonlord, "AskForGeneral")
local generals = table.random(room.general_pile, #nonlord * generalNum)
for i, p in ipairs(nonlord) do
local arg = table.slice(generals, (i - 1) * generalNum + 1, i * generalNum + 1)
p.request_data = json.encode{ arg, n }
p.default_reply = table.random(arg, n)
req:setData(p, { arg, n })
req:setDefaultReply(p, table.random(arg, n))
end
room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord)
for _, p in ipairs(nonlord) do
local general, deputy
if p.general == "" and p.reply_ready then
local general_ret = json.decode(p.client_reply)
general = general_ret[1]
deputy = general_ret[2]
else
general = p.default_reply[1]
deputy = p.default_reply[2]
end
local result = req:getResult(p)
local general, deputy = result[1], result[2]
room:findGeneral(general)
room:findGeneral(deputy)
room:prepareGeneral(p, general, deputy)
p.default_reply = ""
end
room:askForChooseKingdom(nonlord)
@ -1252,6 +1250,7 @@ local role_mode = fk.CreateGameMode{
minPlayer = 2,
maxPlayer = 8,
logic = role_getlogic,
main_mode = "role_mode",
is_counted = function(self, room)
return #room.players >= 5
end,

View File

@ -1,87 +1,71 @@
-- TODO: 合法性的方便函数
-- TODO: 关于如何选择多个目标
-- TODO: 关于装备牌
SmartAI:setCardSkillAI("slash_skill", {
estimated_benefit = 120,
-- 基本牌:杀,闪,桃
on_effect = function(self, logic, effect)
self.skill:onEffect(logic, effect)
end,
})
---@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
SmartAI:setTriggerSkillAI("#nioh_shield_skill", {
correct_func = function(self, logic, event, target, player, data)
return self.skill:triggerable(event, target, player, data)
end,
})
--[=====[
local just_use = {
name = "__just_use",
will_use = Util.TrueFunc,
choose_targets = function(skill, ai, card)
return ai:doOKButton()
end,
}
local use_to_friend = {
name = "__use_to_friend",
will_use = Util.TrueFunc,
choose_targets = function(skill, ai, card)
local targets = ai:getEnabledTargets()
for _, p in ipairs(targets) do
if ai:isFriend(p) then
ai:selectTarget(p, true)
break
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
return ai:doOKButton()
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
local use_to_enemy = {
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
return self:buildUseReply(slashes[1].id, targets)
end
return ai:doOKButton()
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:setSkillAI("__just_use", just_use)
SmartAI:setSkillAI("__use_to_enemy", use_to_enemy)
SmartAI:setSkillAI("__use_to_friend", use_to_friend)
SmartAI:setSkillAI("slash_skill", use_to_enemy)
SmartAI:setSkillAI("dismantlement_skill", use_to_enemy)
SmartAI:setSkillAI("snatch_skill", use_to_enemy)
SmartAI:setSkillAI("duel_skill", use_to_enemy)
SmartAI:setSkillAI("indulgence_skill", use_to_enemy)
SmartAI:setSkillAI("jink_skill", just_use)
SmartAI:setSkillAI("peach_skill", just_use)
SmartAI:setSkillAI("ex_nihilo_skill", just_use)
SmartAI:setSkillAI("savage_assault_skill", just_use)
SmartAI:setSkillAI("archery_attack_skill", just_use)
SmartAI:setSkillAI("god_salvation_skill", just_use)
SmartAI:setSkillAI("amazing_grace_skill", just_use)
SmartAI:setSkillAI("lightning_skill", just_use)
SmartAI:setSkillAI("default_equip_skill", just_use)
--]=====]

View File

@ -26,10 +26,12 @@ Fk:loadTranslationTable{
["basic_char"] = "",
["trick_char"] = "",
["equip_char"] = "",
["non_basic_char"] = "非基",
["basic"] = "基本牌",
["trick"] = "锦囊牌",
["equip"] = "装备牌",
["non_basic"] = "非基本牌",
["weapon"] = "武器牌",
["armor"] = "防具牌",
["defensive_horse"] = "防御坐骑牌",

View File

@ -462,7 +462,6 @@ extension:addCards({
local nullificationSkill = fk.CreateActiveSkill{
name = "nullification_skill",
can_use = Util.FalseFunc,
on_use = function() RoomInstance:delay(1200) end,
on_effect = function(self, room, effect)
if effect.responseToEvent then
effect.responseToEvent.isCancellOut = true
@ -713,7 +712,7 @@ local lightningSkill = fk.CreateActiveSkill{
}
room:moveCards{
ids = Card:getIdList(effect.card),
ids = room:getSubcardsByRule(effect.card, { Card.Processing }),
toArea = Card.DiscardPile,
moveReason = fk.ReasonUse
}

View File

@ -46,9 +46,19 @@ Client::~Client() {
}
void Client::connectToHost(const QString &server, ushort port) {
start_connent_timestamp = QDateTime::currentMSecsSinceEpoch();
router->getSocket()->connectToHost(server, port);
}
void Client::setupServerLag(qint64 server_time) {
auto now = QDateTime::currentMSecsSinceEpoch();
auto ping = now - start_connent_timestamp;
auto lag = now - server_time;
server_lag = lag - ping / 2;
}
qint64 Client::getServerLag() const { return server_lag; }
void Client::replyToServer(const QString &command, const QString &jsonData) {
int type = Router::TYPE_REPLY | Router::SRC_CLIENT | Router::DEST_SERVER;
router->reply(type, command, jsonData);

View File

@ -17,6 +17,8 @@ public:
~Client();
void connectToHost(const QString &server, ushort port);
void setupServerLag(qint64 server_time);
qint64 getServerLag() const;
Q_INVOKABLE void replyToServer(const QString &command, const QString &jsonData);
Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData);
@ -51,6 +53,8 @@ private:
Router *router;
QMap<int, ClientPlayer *> players;
ClientPlayer *self;
qint64 start_connent_timestamp; // 连接时的时间戳 单位毫秒
qint64 server_lag = 0; // 与服务器时差,单位毫秒,正数表示自己快了 负数表示慢了
lua_State *L;
QFileSystemWatcher fsWatcher;

View File

@ -212,6 +212,7 @@ void Server::setupPlayer(ServerPlayer *player, bool all_info) {
arr << player->getId();
arr << player->getScreenName();
arr << player->getAvatar();
arr << QDateTime::currentMSecsSinceEpoch();
player->doNotify("Setup", JsonArray2Bytes(arr));
if (all_info) {

View File

@ -18,6 +18,8 @@ extern QmlBackend *Backend;
%nodefaultdtor Client;
class Client : public QObject {
public:
void setupServerLag(long long server_time);
void replyToServer(const QString &command, const QString &json_data);
void notifyServer(const QString &command, const QString &json_data);

View File

@ -63,7 +63,7 @@ public:
QVariant ret;
if (high < 0) {
if (low < 1) {
ret.setValue($self->bounded(0, 100001) / 100000);
ret.setValue(qreal($self->bounded(0, 100000001)) / 100000000);
} else {
ret.setValue($self->bounded(1, low + 1));
}

View File

@ -289,16 +289,21 @@ QVariant QmlBackend::callLuaFunction(const QString &func_name,
if (!ClientInstance) return QVariantMap();
lua_State *L = ClientInstance->getLuaState();
lua_getglobal(L, "debug");
lua_getfield(L, -1, "traceback");
lua_replace(L, -2);
lua_getglobal(L, func_name.toLatin1().data());
foreach (QVariant v, params) {
pushLuaValue(L, v);
}
int err = lua_pcall(L, params.length(), 1, 0);
int err = lua_pcall(L, params.length(), 1, -params.length() - 2);
if (err) {
qCritical() << lua_tostring(L, -1);
lua_pop(L, 1);
lua_pop(L, 2);
return QVariant();
}
auto result = readLuaValue(L);
@ -649,7 +654,10 @@ QJsonObject QmlBackend::getRequestData() const {
auto router = ClientInstance->getRouter();
obj["id"] = router->getRequestId();
obj["timeout"] = router->getTimeout();
obj["timestamp"] = router->getRequestTimestamp();
auto timestamp = router->getRequestTimestamp();
// 因为timestamp是服务器发来的时间如果自己比服务器的时钟快的话那么就得加上这个差值才行
timestamp += ClientInstance->getServerLag();
obj["timestamp"] = timestamp;
return obj;
}