提供了一个简单的事件记录器机制和一个功能简单的查询函数。
在GameEvent的clear环节中,先执行默认的clear函数,再执行用户自订的clear函数。
This commit is contained in:
notify 2023-06-09 01:10:16 +08:00 committed by GitHub
parent 73fcb765d4
commit 713bbca17a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 306 additions and 56 deletions

View File

@ -167,10 +167,6 @@
<source>updated packages for md5</source> <source>updated packages for md5</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Are you sure to exit?</source>
<translation>退</translation>
</message>
</context> </context>
<context> <context>
@ -179,6 +175,10 @@
<source>FreeKill</source> <source>FreeKill</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Are you sure to exit?</source>
<translation>退</translation>
</message>
</context> </context>
<context> <context>

View File

@ -303,6 +303,7 @@ Fk:loadTranslationTable{
-- phase -- phase
["#PhaseSkipped"] = "%from 跳过了 %arg", ["#PhaseSkipped"] = "%from 跳过了 %arg",
["#GainAnExtraTurn"] = "%from 开始进行一个额外的回合", ["#GainAnExtraTurn"] = "%from 开始进行一个额外的回合",
["#GainAnExtraPhase"] = "%from 开始进行一个额外的 %arg",
-- useCard -- useCard
["#UseCard"] = "%from 使用了牌 %card", ["#UseCard"] = "%from 使用了牌 %card",

View File

@ -59,6 +59,10 @@ function General:initialize(package, name, kingdom, hp, maxHp, gender)
package:addGeneral(self) package:addGeneral(self)
end end
function General:__tostring()
return string.format("<General %s>", self.name)
end
--- 为武将增加技能,需要注意增加其他武将技能时的处理方式。 --- 为武将增加技能,需要注意增加其他武将技能时的处理方式。
---@param skill Skill @ (单个)武将技能 ---@param skill Skill @ (单个)武将技能
function General:addSkill(skill) function General:addSkill(skill)

View File

@ -444,6 +444,18 @@ function Player:inMyAttackRange(other, fixLimit)
return self:distanceTo(other) <= (baseAttackRange + fixLimit) return self:distanceTo(other) <= (baseAttackRange + fixLimit)
end end
function Player:getNextAlive()
if Fk:currentRoom().alive_players == 0 then
return self
end
local ret = self.next
while ret.dead do
ret = ret.next
end
return ret
end
--- 增加玩家使用特定牌的历史次数。 --- 增加玩家使用特定牌的历史次数。
---@param cardName string @ 牌名 ---@param cardName string @ 牌名
---@param num integer @ 次数 ---@param num integer @ 次数

View File

@ -1,6 +1,32 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
local Util = {} local Util = {}
Util.DummyFunc = function() end
Util.DummyTable = setmetatable({}, {
__newindex = function() error("Cannot assign to dummy table") end
})
local metamethods = {
"__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv",
"__band", "__bor", "__bxor", "__bnot", "__shl", "__shr",
"__concat", "__len", "__eq", "__lt", "__le", "__call",
-- "__index", "__newindex",
}
-- 别对类用 暂且会弄坏isSubclassOf 懒得研究先
Util.lockTable = function(t)
local mt = getmetatable(t) or Util.DummyTable
local new_mt = {
__index = t,
__newindex = function() error("Cannot assign to locked table") end,
__metatable = false,
}
for _, e in ipairs(metamethods) do
new_mt[e] = mt[e]
end
return setmetatable({}, new_mt)
end
function printf(fmt, ...) print(string.format(fmt, ...)) end
-- the iterator of QList object -- the iterator of QList object
local qlist_iterator = function(list, n) local qlist_iterator = function(list, n)
@ -150,13 +176,13 @@ function table.simpleClone(self)
return ret return ret
end end
-- similar to table.clone but convert all class/instances to string -- similar to table.clone but not clone class/instances
function table.cloneWithoutClass(self) function table.cloneWithoutClass(self)
local ret = {} local ret = {}
for k, v in pairs(self) do for k, v in pairs(self) do
if type(v) == "table" then if type(v) == "table" then
if v.class or v.super then if v.class or v.super then
ret[k] = tostring(v) ret[k] = v
else else
ret[k] = table.cloneWithoutClass(v) ret[k] = table.cloneWithoutClass(v)
end end

View File

@ -77,6 +77,9 @@ local function _call(self, ...) return self:new(...) end
local function _createClass(name, super) local function _createClass(name, super)
local dict = {} local dict = {}
dict.__index = dict dict.__index = dict
--[[ debug
dict.__gc = function(t) printf("%s destructed", tostring(t)) end
--]]
local aClass = { name = name, super = super, static = {}, local aClass = { name = name, super = super, static = {},
__instanceDict = dict, __declaredMethods = {}, __instanceDict = dict, __declaredMethods = {},

View File

@ -111,4 +111,7 @@ fk.CardShown = 77
-- 79 = BeforeTurnOver -- 79 = BeforeTurnOver
-- 80 = BeforeChainStateChange -- 80 = BeforeChainStateChange
fk.NumOfEvents = 81 fk.SkillEffect = 81
fk.AfterSkillEffect = 82
fk.NumOfEvents = 83

View File

@ -2,6 +2,9 @@
-- Definitions of game events -- Definitions of game events
-- 某类事件对应的结束事件其id刚好就是那个事件的相反数
-- GameEvent.EventFinish = -1
GameEvent.ChangeHp = 1 GameEvent.ChangeHp = 1
GameEvent.Damage = 2 GameEvent.Damage = 2
GameEvent.LoseHp = 3 GameEvent.LoseHp = 3

View File

@ -1,6 +1,16 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.SkillEffect] = function(self) GameEvent.functions[GameEvent.SkillEffect] = function(self)
local effect_cb = table.unpack(self.data) local effect_cb, player, skill = table.unpack(self.data)
return effect_cb() local room = self.room
local logic = room.logic
local cost_data_bak = skill.cost_data
logic:trigger(fk.SkillEffect, player, skill)
skill.cost_data = cost_data_bak
local ret = effect_cb()
logic:trigger(fk.AfterSkillEffect, player, skill)
return ret
end end

View File

@ -1,40 +1,70 @@
-- SPDX-License-Identifier: GPL-3.0-or-later -- SPDX-License-Identifier: GPL-3.0-or-later
---@class GameEvent: Object ---@class GameEvent: Object
---@field public room Room ---@field public id integer @ 事件的id随着时间推移自动增加并分配给新事件
---@field public event integer ---@field public end_id integer @ 事件的对应结束id如果整个事件中未插入事件那么end_id就是自己的id
---@field public data any ---@field public room Room @ room实例
---@field public parent GameEvent ---@field public event integer @ 该事件对应的EventType
---@field public main_func fun(self: GameEvent) ---@field public data any @ 事件的附加数据,视类型而定
---@field public clear_func fun(self: GameEvent) ---@field public parent GameEvent @ 事件的父事件(栈中的上一层事件)
---@field public extra_clear_funcs any[] ---@field public main_func fun(self: GameEvent) @ 事件的主函数
---@field public interrupted boolean ---@field public clear_func fun(self: GameEvent) @ 事件结束时执行的函数
---@field public extra_clear_funcs fun(self:GameEvent)[] @ 事件结束时执行的自定义函数列表
---@field public exit_func fun(self: GameEvent) @ 事件结束后执行的函数
---@field public extra_exit_funcs fun(self:GameEvent)[] @ 事件结束后执行的自定义函数
---@field public interrupted boolean @ 事件是否是因为被强行中断而结束的
local GameEvent = class("GameEvent") local GameEvent = class("GameEvent")
---@type fun(self: GameEvent)[]
GameEvent.functions = {} GameEvent.functions = {}
---@type fun(self: GameEvent)[]
GameEvent.cleaners = {} GameEvent.cleaners = {}
---@type fun(self: GameEvent)[]
GameEvent.exit_funcs = {}
local function wrapCoFunc(f, ...) local function wrapCoFunc(f, ...)
if not f then return nil end if not f then return nil end
local args = {...} local args = {...}
return function() return f(table.unpack(args)) end return function() return f(table.unpack(args)) end
end end
local function dummyFunc() end local dummyFunc = Util.DummyFunc
function GameEvent:initialize(event, ...) function GameEvent:initialize(event, ...)
self.id = -1
self.end_id = -1
self.room = RoomInstance self.room = RoomInstance
self.event = event self.event = event
self.data = { ... } self.data = { ... }
self.main_func = wrapCoFunc(GameEvent.functions[event], self) or dummyFunc self.main_func = wrapCoFunc(GameEvent.functions[event], self) or dummyFunc
self.clear_func = GameEvent.cleaners[event] or dummyFunc self.clear_func = GameEvent.cleaners[event] or dummyFunc
self.extra_clear_funcs = {} self.extra_clear_funcs = Util.DummyTable
self.exit_func = GameEvent.exit_funcs[event] or dummyFunc
self.extra_exit_funcs = Util.DummyTable
self.interrupted = false self.interrupted = false
end end
function GameEvent:__tostring() function GameEvent:__tostring()
return GameEvent:translate(self.event) return string.format("<%s #%d>", GameEvent:translate(self.event), self.id)
end end
function GameEvent:findParent(eventType) function GameEvent:addCleaner(f)
if self.extra_clear_funcs == Util.DummyTable then
self.extra_clear_funcs = {}
end
table.insert(self.extra_clear_funcs, f)
end
function GameEvent:addExitFunc(f)
if self.extra_exit_funcs == Util.DummyTable then
self.extra_exit_funcs = {}
end
table.insert(self.extra_exit_funcs, f)
end
function GameEvent:findParent(eventType, includeSelf)
if includeSelf and self.event == eventType then return self end
local e = self.parent local e = self.parent
repeat repeat
if e.event == eventType then return e end if e.event == eventType then return e end
@ -43,12 +73,83 @@ function GameEvent:findParent(eventType)
return nil return nil
end end
-- 找n个id介于from和to之间的事件。
local function bin_search(events, from, to, n, func)
local left = 1
local right = #events
local mid
local ret = {}
if from < events[1].id then
mid = 1
elseif from > events[right].id then
return ret
else
while true do
if left > right then return ret end
mid = (left + right) // 2
local id = events[mid].id
local id_left = mid == 1 and -math.huge or events[mid - 1].id
if from < id then
if from >= id_left then
break
end
right = mid - 1
else
left = mid + 1
end
end
end
for i = mid, #events do
local v = events[i]
if v.id < to and func(v) then
table.insert(ret, v)
end
if #ret >= n then break end
end
return ret
end
-- 从某个区间中找出类型符合且符合func函数检测的至多n个事件。
---@param eventType integer @ 要查找的事件类型
---@param n integer @ 最多找多少个
---@param func fun(e: GameEvent): boolean @ 过滤用的函数
---@param endEvent GameEvent|nil @ 区间终止点,默认为本事件结束
---@return GameEvent[] @ 找到的符合条件的所有事件最多n个但不保证有n个
function GameEvent:searchEvents(eventType, n, func, endEvent)
local logic = self.room.logic
local events = logic.event_recorder[eventType]
local from = self.id
local to = endEvent and endEvent.id or self.end_id
if to == -1 then to = #logic.all_game_events end
n = n or 1
func = func or function() return true end
local ret
if #events < 6 then
ret = {}
for _, v in ipairs(events) do
if v.id > from and v.id < to and func(v) then
table.insert(ret, v)
end
if #ret >= n then break end
end
else
ret = bin_search(events, from, to, n, func)
end
return ret
end
function GameEvent:clear() function GameEvent:clear()
local clear_co = coroutine.create(function() local clear_co = coroutine.create(function()
self:clear_func()
for _, f in ipairs(self.extra_clear_funcs) do for _, f in ipairs(self.extra_clear_funcs) do
if type(f) == "function" then f(self) end if type(f) == "function" then f(self) end
end end
self:clear_func()
end) end)
while true do while true do
@ -72,6 +173,28 @@ function GameEvent:clear()
break break
end end
end end
local logic = RoomInstance.logic
local end_id = logic.current_event_id + 1
if self.id ~= end_id - 1 then
logic.all_game_events[end_id] = -self.event
logic.current_event_id = end_id
self.end_id = end_id
else
self.end_id = self.id
end
logic.game_event_stack:pop()
local err, msg
err, msg = xpcall(self.exit_func, debug.traceback, self)
if err == false then fk.qCritical(msg) end
for _, f in ipairs(self.extra_exit_funcs) do
if type(f) == "function" then
err, msg = xpcall(f, debug.traceback, self)
if err == false then fk.qCritical(msg) end
end
end
end end
local function breakEvent(self, extra_yield_result) local function breakEvent(self, extra_yield_result)
@ -95,6 +218,12 @@ function GameEvent:exec()
self.parent = logic:getCurrentEvent() self.parent = logic:getCurrentEvent()
logic.game_event_stack:push(self) logic.game_event_stack:push(self)
logic.current_event_id = logic.current_event_id + 1
self.id = logic.current_event_id
logic.all_game_events[self.id] = self
logic.event_recorder[self.event] = logic.event_recorder[self.event] or {}
table.insert(logic.event_recorder[self.event], self)
local co = coroutine.create(self.main_func) local co = coroutine.create(self.main_func)
while true do while true do
local err, yield_result, extra_yield_result = coroutine.resume(co) local err, yield_result, extra_yield_result = coroutine.resume(co)
@ -123,7 +252,7 @@ function GameEvent:exec()
-- yield to corresponding GameEvent, first pop self from stack -- yield to corresponding GameEvent, first pop self from stack
self.interrupted = true self.interrupted = true
self:clear() self:clear()
logic.game_event_stack:pop(self) -- logic.game_event_stack:pop(self)
coroutine.close(co) coroutine.close(co)
-- then, call yield -- then, call yield
@ -151,7 +280,6 @@ function GameEvent:exec()
end end
end end
logic.game_event_stack:pop(self)
return ret, extra_ret return ret, extra_ret
end end

View File

@ -3,12 +3,14 @@
---@class GameLogic: Object ---@class GameLogic: Object
---@field public room Room ---@field public room Room
---@field public skill_table table<Event, TriggerSkill[]> ---@field public skill_table table<Event, TriggerSkill[]>
---@field public skill_priority_table<Event, number[]> ---@field public skill_priority_table table<Event, number[]>
---@field public refresh_skill_table table<Event, TriggerSkill[]> ---@field public refresh_skill_table table<Event, TriggerSkill[]>
---@field public skills string[] ---@field public skills string[]
---@field public event_stack Stack
---@field public game_event_stack Stack ---@field public game_event_stack Stack
---@field public role_table string[][] ---@field public role_table string[][]
---@field public all_game_events GameEvent[]
---@field public event_recorder table<integer, GameEvent>
---@field public current_event_id integer
local GameLogic = class("GameLogic") local GameLogic = class("GameLogic")
function GameLogic:initialize(room) function GameLogic:initialize(room)
@ -17,8 +19,10 @@ function GameLogic:initialize(room)
self.skill_priority_table = {} self.skill_priority_table = {}
self.refresh_skill_table = {} self.refresh_skill_table = {}
self.skills = {} -- skillName[] self.skills = {} -- skillName[]
self.event_stack = Stack:new()
self.game_event_stack = Stack:new() self.game_event_stack = Stack:new()
self.all_game_events = {}
self.event_recorder = {}
self.current_event_id = 0
self.role_table = { self.role_table = {
{ "lord" }, { "lord" },
@ -381,8 +385,6 @@ function GameLogic:trigger(event, target, data, refresh_only)
local _target = room.current -- for iteration local _target = room.current -- for iteration
local player = _target local player = _target
self.event_stack:push({event, target, data})
if #skills_to_refresh > 0 then repeat do if #skills_to_refresh > 0 then repeat do
-- refresh skills. This should not be broken -- refresh skills. This should not be broken
for _, skill in ipairs(skills_to_refresh) do for _, skill in ipairs(skills_to_refresh) do
@ -450,7 +452,6 @@ function GameLogic:trigger(event, target, data, refresh_only)
::trigger_loop_continue:: ::trigger_loop_continue::
end end
self.event_stack:pop()
return broken return broken
end end
@ -459,6 +460,29 @@ function GameLogic:getCurrentEvent()
return self.game_event_stack.t[self.game_event_stack.p] return self.game_event_stack.t[self.game_event_stack.p]
end end
-- 在指定历史范围中找至多n个符合条件的事件
---@param eventType integer @ 要查找的事件类型
---@param n integer @ 最多找多少个
---@param func fun(e: GameEvent): boolean @ 过滤用的函数
---@param scope integer @ 查询历史范围,只能是当前阶段/回合/轮次
---@return GameEvent[] @ 找到的符合条件的所有事件最多n个但不保证有n个
function GameLogic:getEventsOfScope(eventType, n, func, scope)
scope = scope or Player.HistoryTurn
local event = self:getCurrentEvent()
local start_event ---@type GameEvent
if scope == Player.HistoryGame then
start_event = self.all_game_events[1]
elseif scope == Player.HistoryRound then
start_event = event:findParent(GameEvent.Round)
elseif scope == Player.HistoryTurn then
start_event = event:findParent(GameEvent.Turn)
elseif scope == Player.HistoryPhase then
start_event = event:findParent(GameEvent.Phase)
end
return start_event:searchEvents(eventType, n, func)
end
function GameLogic:dumpEventStack(detailed) function GameLogic:dumpEventStack(detailed)
local top = self:getCurrentEvent() local top = self:getCurrentEvent()
local i = self.game_event_stack.p local i = self.game_event_stack.p
@ -493,6 +517,28 @@ function GameLogic:dumpEventStack(detailed)
print("\n===== End of event stack dump =====") print("\n===== End of event stack dump =====")
end end
function GameLogic:dumpAllEvents(from, to)
from = from or 1
to = to or #self.all_game_events
assert(from <= to)
local indent = 0
local tab = " "
for i = from, to, 1 do
local v = self.all_game_events[i]
if type(v) == "number" then
indent = math.max(indent - 1, 0)
-- v = "End"
-- print(tab:rep(indent) .. string.format("#%d: %s", i, v))
else
print(tab:rep(indent) .. string.format("%s", tostring(v)))
if v.id ~= v.end_id then
indent = indent + 1
end
end
end
end
function GameLogic:breakEvent(ret) function GameLogic:breakEvent(ret)
coroutine.yield("__breakEvent", ret) coroutine.yield("__breakEvent", ret)
end end

View File

@ -62,8 +62,7 @@ dofile "lua/server/ai/init.lua"
---@param _room fk.Room ---@param _room fk.Room
function Room:initialize(_room) function Room:initialize(_room)
self.room = _room self.room = _room
_room.startGame = function(_self)
self.room.startGame = function(_self)
Room.initialize(self, _room) -- clear old data Room.initialize(self, _room) -- clear old data
self.settings = json.decode(_room:settings()) self.settings = json.decode(_room:settings())
Fk.disabled_packs = self.settings.disabledPack Fk.disabled_packs = self.settings.disabledPack
@ -1387,7 +1386,7 @@ function Room:handleUseCardReply(player, data)
Self = player Self = player
local c = skill:viewAs(selected_cards) local c = skill:viewAs(selected_cards)
if c then if c then
self:useSkill(player, skill) self:useSkill(player, skill, Util.DummyFunc)
local use = {} ---@type CardUseStruct local use = {} ---@type CardUseStruct
use.from = player.id use.from = player.id
@ -2177,7 +2176,7 @@ function Room:handleCardEffect(event, cardEffectEvent)
if cardEffectEvent.card.skill then if cardEffectEvent.card.skill then
execGameEvent(GameEvent.SkillEffect, function () execGameEvent(GameEvent.SkillEffect, function ()
cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) cardEffectEvent.card.skill:onEffect(self, cardEffectEvent)
end) end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill)
end end
end end
end end
@ -2691,7 +2690,7 @@ function Room:useSkill(player, skill, effect_cb)
player:addSkillUseHistory(skill.name) player:addSkillUseHistory(skill.name)
if effect_cb then if effect_cb then
return execGameEvent(GameEvent.SkillEffect, effect_cb) return execGameEvent(GameEvent.SkillEffect, effect_cb, player, skill)
end end
end end

View File

@ -322,18 +322,6 @@ function ServerPlayer:isAlive()
return self.dead == false return self.dead == false
end end
function ServerPlayer:getNextAlive()
if #self.room.alive_players == 0 then
return self
end
local ret = self.next
while ret.dead do
ret = ret.next
end
return ret
end
function ServerPlayer:turnOver() function ServerPlayer:turnOver()
if self.room.logic:trigger(fk.BeforeTurnOver, self) then if self.room.logic:trigger(fk.BeforeTurnOver, self) then
return return
@ -367,6 +355,13 @@ function ServerPlayer:showCards(cards)
room.logic:trigger(fk.CardShown, self, { cardIds = cards }) room.logic:trigger(fk.CardShown, self, { cardIds = cards })
end end
local phase_name_table = {
[Player.Judge] = "phase_judge",
[Player.Draw] = "phase_draw",
[Player.Play] = "phase_play",
[Player.Discard] = "phase_discard",
}
---@param from_phase Phase ---@param from_phase Phase
---@param to_phase Phase ---@param to_phase Phase
function ServerPlayer:changePhase(from_phase, to_phase) function ServerPlayer:changePhase(from_phase, to_phase)
@ -397,25 +392,35 @@ function ServerPlayer:changePhase(from_phase, to_phase)
return false return false
end end
function ServerPlayer:gainAnExtraPhase(phase) function ServerPlayer:gainAnExtraPhase(phase, delay)
local room = self.room local room = self.room
delay = (delay == nil) and true or delay
if delay then
local logic = room.logic
local turn = logic:getCurrentEvent():findParent(GameEvent.Phase, true)
if turn then
turn:addExitFunc(function() self:gainAnExtraPhase(phase, false) end)
return
end
end
local current = self.phase local current = self.phase
self.phase = phase self.phase = phase
room:notifyProperty(self, self, "phase") room:notifyProperty(self, self, "phase")
room:sendLog{
type = "#GainAnExtraPhase",
from = self.id,
arg = phase_name_table[phase],
}
GameEvent(GameEvent.Phase, self):exec() GameEvent(GameEvent.Phase, self):exec()
self.phase = current self.phase = current
room:notifyProperty(self, self, "phase") room:notifyProperty(self, self, "phase")
end end
local phase_name_table = {
[Player.Judge] = "phase_judge",
[Player.Draw] = "phase_draw",
[Player.Play] = "phase_play",
[Player.Discard] = "phase_discard",
}
---@param phase_table Phase[] ---@param phase_table Phase[]
function ServerPlayer:play(phase_table) function ServerPlayer:play(phase_table)
phase_table = phase_table or {} phase_table = phase_table or {}
@ -505,8 +510,18 @@ function ServerPlayer:skip(phase)
end end
end end
function ServerPlayer:gainAnExtraTurn() function ServerPlayer:gainAnExtraTurn(delay)
local room = self.room local room = self.room
delay = (delay == nil) and true or delay
if delay then
local logic = room.logic
local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true)
if turn then
turn:addExitFunc(function() self:gainAnExtraTurn(false) end)
return
end
end
room:sendLog{ room:sendLog{
type = "#GainAnExtraTurn", type = "#GainAnExtraTurn",
from = self.id from = self.id