-- SPDX-License-Identifier: GPL-3.0-or-later ---@class GameLogic: Object ---@field public room Room ---@field public skill_table table ---@field public skill_priority_table table ---@field public refresh_skill_table table ---@field public skills string[] ---@field public game_event_stack Stack ---@field public role_table string[][] ---@field public all_game_events GameEvent[] ---@field public event_recorder table ---@field public current_event_id integer local GameLogic = class("GameLogic") function GameLogic:initialize(room) self.room = room self.skill_table = {} -- TriggerEvent --> TriggerSkill[] self.skill_priority_table = {} self.refresh_skill_table = {} self.skills = {} -- skillName[] self.game_event_stack = Stack:new() self.all_game_events = {} self.event_recorder = {} self.current_event_id = 0 self.role_table = { { "lord" }, { "lord", "rebel" }, { "lord", "rebel", "renegade" }, { "lord", "loyalist", "rebel", "renegade" }, { "lord", "loyalist", "rebel", "rebel", "renegade" }, { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" }, { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" }, { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" }, } end function GameLogic:run() -- default logic local room = self.room table.shuffle(self.room.players) self:assignRoles() self.room.game_started = true room:doBroadcastNotify("StartGame", "") room:adjustSeats() self:chooseGenerals() self:buildPlayerCircle() self:broadcastGeneral() self:prepareDrawPile() self:attachSkillToPlayers() self:prepareForStart() self:action() end local function execGameEvent(type, ...) local event = GameEvent:new(type, ...) local _, ret = event:exec() return ret end function GameLogic:assignRoles() local room = self.room local n = #room.players local roles = self.role_table[n] table.shuffle(roles) for i = 1, n do local p = room.players[i] p.role = roles[i] if p.role == "lord" then p.role_shown = true room:broadcastProperty(p, "role") else room:notifyProperty(p, p, "role") end end end function GameLogic:chooseGenerals() local room = self.room local generalNum = room.settings.generalNum local n = room.settings.enableDeputy and 2 or 1 local lord = room:getLord() local lord_generals = {} if lord ~= nil then room.current = lord local generals = {} local lordlist = {} local lordpools = {} if room.settings.gameMode == "aaa_role_mode" then for _, general in pairs(Fk:getAllGenerals()) do if (not general.hidden and not general.total_hidden) and table.find(general.skills, function(s) return s.lordSkill end) and not table.find(lordlist, function(g) return g.trueName == general.trueName end) then table.insert(lordlist, general) end end lordlist = table.random(lordlist, 3) or {} end table.insertTable(generals, Fk:getGeneralsRandomly(generalNum, Fk:getAllGenerals(), nil, function(g) return table.contains(table.map(lordlist, function(g) return g.trueName end), g.trueName) end)) for i = 1, #generals do generals[i] = generals[i].name end lordpools = table.simpleClone(generals) table.insertTable(lordpools, table.map(lordlist, function(g) return g.name end)) lord_generals = room:askForGeneral(lord, lordpools, n) local lord_general, deputy if type(lord_generals) == "table" then deputy = lord_generals[2] lord_general = lord_generals[1] else lord_general = lord_generals lord_generals = {lord_general} end room:setPlayerGeneral(lord, lord_general, true) room:askForChooseKingdom({lord}) room:broadcastProperty(lord, "general") room:broadcastProperty(lord, "kingdom") room:setDeputyGeneral(lord, deputy) room:broadcastProperty(lord, "deputyGeneral") end local nonlord = room:getOtherPlayers(lord, true) local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals) table.shuffle(generals) for _, p in ipairs(nonlord) do local arg = {} for i = 1, generalNum do table.insert(arg, table.remove(generals, 1).name) end p.request_data = json.encode{ arg, n } p.default_reply = table.random(arg, n) end room:notifyMoveFocus(nonlord, "AskForGeneral") room:doBroadcastRequest("AskForGeneral", nonlord) for _, p in ipairs(nonlord) do if p.general == "" and p.reply_ready then local generals = json.decode(p.client_reply) local general = generals[1] local deputy = generals[2] room:setPlayerGeneral(p, general, true, true) room:setDeputyGeneral(p, deputy) else room:setPlayerGeneral(p, p.default_reply[1], true, true) room:setDeputyGeneral(p, p.default_reply[2]) end p.default_reply = "" end room:askForChooseKingdom(nonlord) end function GameLogic:buildPlayerCircle() local room = self.room local players = room.players room.alive_players = {table.unpack(players)} for i = 1, #players - 1 do players[i].next = players[i + 1] end players[#players].next = players[1] end function GameLogic:broadcastGeneral() local room = self.room local players = room.players for _, p in ipairs(players) do assert(p.general ~= "") local general = Fk.generals[p.general] local deputy = Fk.generals[p.deputyGeneral] p.maxHp = p:getGeneralMaxHp() p.hp = deputy and math.floor((deputy.hp + general.hp) / 2) or general.hp p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5) -- TODO: setup AI here if p.role ~= "lord" then room:broadcastProperty(p, "general") room:broadcastProperty(p, "kingdom") room:broadcastProperty(p, "deputyGeneral") elseif #players >= 5 then p.maxHp = p.maxHp + 1 p.hp = p.hp + 1 end room:broadcastProperty(p, "maxHp") room:broadcastProperty(p, "hp") room:broadcastProperty(p, "shield") end end function GameLogic:prepareDrawPile() local room = self.room local allCardIds = Fk:getAllCardIds() for i = #allCardIds, 1, -1 do if Fk:getCardById(allCardIds[i]).is_derived then local id = allCardIds[i] table.removeOne(allCardIds, id) table.insert(room.void, id) room:setCardArea(id, Card.Void, nil) end end table.shuffle(allCardIds) room.draw_pile = allCardIds for _, id in ipairs(room.draw_pile) do self.room:setCardArea(id, Card.DrawPile, nil) end end function GameLogic:attachSkillToPlayers() local room = self.room local players = room.players local addRoleModSkills = function(player, skillName) local skill = Fk.skills[skillName] if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then return end if #skill.attachedKingdom > 0 and not table.contains(skill.attachedKingdom, player.kingdom) then return end room:handleAddLoseSkills(player, skillName, nil, false) end for _, p in ipairs(room.alive_players) do local skills = Fk.generals[p.general].skills for _, s in ipairs(skills) do addRoleModSkills(p, s.name) end for _, sname in ipairs(Fk.generals[p.general].other_skills) do addRoleModSkills(p, sname) end local deputy = Fk.generals[p.deputyGeneral] if deputy then skills = deputy.skills for _, s in ipairs(skills) do addRoleModSkills(p, s.name) end for _, sname in ipairs(deputy.other_skills) do addRoleModSkills(p, sname) end end end end function GameLogic:prepareForStart() local room = self.room local players = room.players self:addTriggerSkill(GameRule) for _, trig in ipairs(Fk.global_trigger) do self:addTriggerSkill(trig) end self.room:sendLog{ type = "$GameStart" } end function GameLogic:action() self:trigger(fk.GamePrepared) local room = self.room execGameEvent(GameEvent.DrawInitial) while true do execGameEvent(GameEvent.Round) if room.game_finished then break end end end ---@param skill TriggerSkill function GameLogic:addTriggerSkill(skill) if skill == nil or table.contains(self.skills, skill.name) then return end table.insert(self.skills, skill.name) for _, event in ipairs(skill.refresh_events) do if self.refresh_skill_table[event] == nil then self.refresh_skill_table[event] = {} end table.insert(self.refresh_skill_table[event], skill) end for _, event in ipairs(skill.events) do if self.skill_table[event] == nil then self.skill_table[event] = {} end table.insert(self.skill_table[event], skill) if self.skill_priority_table[event] == nil then self.skill_priority_table[event] = {} end local priority_tab = self.skill_priority_table[event] local prio = skill.priority_table[event] if not table.contains(priority_tab, prio) then for i, v in ipairs(priority_tab) do if v < prio then table.insert(priority_tab, i, prio) break end end if not table.contains(priority_tab, prio) then table.insert(priority_tab, prio) end end if not table.contains(self.skill_priority_table[event], skill.priority_table[event]) then table.insert(self.skill_priority_table[event], skill.priority_table[event]) end end if skill.visible then if (Fk.related_skills[skill.name] == nil) then return end for _, s in ipairs(Fk.related_skills[skill.name]) do if (s.class == TriggerSkill) then self:addTriggerSkill(s) end end end end ---@param event Event ---@param target ServerPlayer|nil ---@param data any|nil function GameLogic:trigger(event, target, data, refresh_only) local room = self.room local broken = false local skills = self.skill_table[event] or {} local skills_to_refresh = self.refresh_skill_table[event] or Util.DummyTable local _target = room.current -- for iteration local player = _target if #skills_to_refresh > 0 then repeat do -- refresh skills. This should not be broken for _, skill in ipairs(skills_to_refresh) do if skill:canRefresh(event, target, player, data) then skill:refresh(event, target, player, data) end end player = player.next end until player == _target end if #skills == 0 or refresh_only then return end local prio_tab = self.skill_priority_table[event] local prev_prio = math.huge for _, prio in ipairs(prio_tab) do if broken then break end if prio >= prev_prio then -- continue goto trigger_loop_continue end repeat do local triggerables = table.filter(skills, function(skill) return skill.priority_table[event] == prio and skill:triggerable(event, target, player, data) end) local skill_names = table.map(triggerables, function(skill) return skill.name end) while #skill_names > 0 do local skill_name = prio <= 0 and table.random(skill_names) or room:askForChoice(player, skill_names, "trigger", "#choose-trigger") local skill = skill_name == "game_rule" and GameRule or Fk.skills[skill_name] local len = #skills broken = skill:trigger(event, target, player, data) table.insertTable( skill_names, table.map(table.filter(table.slice(skills, len - #skills), function(s) return s.priority_table[event] == prio and s:triggerable(event, target, player, data) end), function(s) return s.name end) ) broken = broken or (event == fk.AskForPeaches and room:getPlayerById(data.who).hp > 0) if broken then break end table.removeOne(skill_names, skill_name) end if broken then break end player = player.next end until player == _target prev_prio = prio ::trigger_loop_continue:: end return broken end ---@return GameEvent function GameLogic:getCurrentEvent() return self.game_event_stack.t[self.game_event_stack.p] 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, true) elseif scope == Player.HistoryTurn then start_event = event:findParent(GameEvent.Turn, true) elseif scope == Player.HistoryPhase then start_event = event:findParent(GameEvent.Phase, true) end return start_event:searchEvents(eventType, n, func) end function GameLogic:dumpEventStack(detailed) local top = self:getCurrentEvent() local i = self.game_event_stack.p local inspect = p if not top then return end print("===== Start of event stack dump =====") if not detailed then print("") end repeat local printable_data if type(top.data) ~= "table" then printable_data = top.data else printable_data = table.cloneWithoutClass(top.data) end if not detailed then print("Stack level #" .. i .. ": " .. tostring(top)) else print("\nStack level #" .. i .. ":") inspect{ eventId = GameEvent:translate(top.event), data = printable_data or "nil", } end top = top.parent i = i - 1 until not top print("\n===== End of event stack dump =====") 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) coroutine.yield("__breakEvent", ret) end function GameLogic:breakTurn() local event = self:getCurrentEvent():findParent(GameEvent.Turn) event:shutdown() end return GameLogic