FreeKill/lua/server/gamelogic.lua

333 lines
9.0 KiB
Lua

-- SPDX-License-Identifier: GPL-3.0-or-later
---@class GameLogic: Object
---@field public room Room
---@field public skill_table table<Event, TriggerSkill[]>
---@field public refresh_skill_table table<Event, TriggerSkill[]>
---@field public skills string[]
---@field public event_stack Stack
---@field public game_event_stack Stack
---@field public role_table string[][]
local GameLogic = class("GameLogic")
function GameLogic:initialize(room)
self.room = room
self.skill_table = {} -- TriggerEvent --> TriggerSkill[]
self.refresh_skill_table = {}
self.skills = {} -- skillName[]
self.event_stack = Stack:new()
self.game_event_stack = Stack:new()
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
table.shuffle(self.room.players)
self:assignRoles()
self.room:adjustSeats()
self:chooseGenerals()
self:prepareForStart()
self.room.game_started = true
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 lord = room:getLord()
local lord_general = nil
if lord ~= nil then
room.current = lord
local generals = Fk:getGeneralsRandomly(generalNum)
for i = 1, #generals do
generals[i] = generals[i].name
end
lord_general = room:askForGeneral(lord, generals)
room:setPlayerGeneral(lord, lord_general, true)
room:broadcastProperty(lord, "general")
end
local nonlord = room:getOtherPlayers(lord, true)
local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, {lord_general})
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)
p.default_reply = arg[1]
end
room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord)
for _, p in ipairs(nonlord) do
if p.general == "" and p.reply_ready then
local general = json.decode(p.client_reply)[1]
room:setPlayerGeneral(p, general, true)
else
room:setPlayerGeneral(p, p.default_reply, true)
end
p.default_reply = ""
end
end
function GameLogic:prepareForStart()
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]
for _, p in ipairs(players) do
assert(p.general ~= "")
local general = Fk.generals[p.general]
p.maxHp = general.maxHp
p.hp = general.hp
p.shield = general.shield
-- TODO: setup AI here
if p.role ~= "lord" then
room:broadcastProperty(p, "general")
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
local allCardIds = Fk:getAllCardIds()
table.shuffle(allCardIds)
room.draw_pile = allCardIds
for _, id in ipairs(room.draw_pile) do
self.room:setCardArea(id, Card.DrawPile, nil)
end
local addRoleModSkills = function(player, skillName)
local skill = Fk.skills[skillName]
if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) 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
end
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.GameStart)
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)
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
---@param data any
function GameLogic:trigger(event, target, data)
local room = self.room
local broken = false
local skills = self.skill_table[event] or {}
local skills_to_refresh = self.refresh_skill_table[event] or {}
local _target = target or room.current -- for iteration
local player = _target
self.event_stack:push({event, target, data})
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
---@param a TriggerSkill
---@param b TriggerSkill
local compare_func = function (a, b)
return a.priority_table[event] > b.priority_table[event]
end
table.sort(skills, compare_func)
repeat do
local triggerable_skills = {} ---@type table<number, TriggerSkill[]>
local priority_table = {} ---@type number[]
for _, skill in ipairs(skills) do
if skill:triggerable(event, target, player, data) then
local priority = skill.priority_table[event]
if triggerable_skills[priority] == nil then
triggerable_skills[priority] = {}
end
table.insert(triggerable_skills[priority], skill)
if not table.contains(priority_table, priority) then
table.insert(priority_table, priority)
end
end
end
for _, priority in ipairs(priority_table) do
local triggerables = triggerable_skills[priority]
local skill_names = {} ---@type string[]
for _, skill in ipairs(triggerables) do
table.insert(skill_names, skill.name)
end
while #skill_names > 0 do
local skill_name = room:askForChoice(player, skill_names, "trigger", "#choose-trigger")
local skill = triggerables[table.indexOf(skill_names, skill_name)]
broken = skill:trigger(event, target, player, data)
broken = broken or (event == fk.AskForPeaches and room:getPlayerById(data.who).hp > 0)
if broken then break end
table.removeOne(skill_names, skill_name)
table.removeOne(triggerables, skill)
end
end
if broken then break end
player = player.next
end until player == _target
self.event_stack:pop()
return broken
end
---@return GameEvent
function GameLogic:getCurrentEvent()
return self.game_event_stack.t[self.game_event_stack.p]
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 .. ": " .. GameEvent:translate(top.event))
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:breakEvent(ret)
coroutine.yield("__breakEvent", ret)
end
return GameLogic