武将牌堆和更多bugfix (#261)

- 修复了询问卡牌时会返回不符合要求的牌的bug
- 修复了摸牌阶段的skillname(游戏规则)
- 实装武将牌堆
- 剥离身份模式特有常备主逻辑
- 为资产变换添加时机
- 分离了改判时的移动
- 将canUseGeneral改为Engine所属函数
This commit is contained in:
YoumuKon 2023-09-27 21:02:22 +08:00 committed by GitHub
parent 3c33bcff77
commit 7e15ccb63e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 231 additions and 55 deletions

View File

@ -2,12 +2,12 @@
---@class Client
---@field public client fk.Client
---@field public players ClientPlayer[]
---@field public alive_players ClientPlayer[]
---@field public observers ClientPlayer[]
---@field public current ClientPlayer
---@field public discard_pile integer[]
---@field public status_skills Skill[]
---@field public players ClientPlayer[] @ 所有参战玩家的数组
---@field public alive_players ClientPlayer[] @ 所有存活玩家的数组
---@field public observers ClientPlayer[] @ 观察者的数组
---@field public current ClientPlayer @ 当前回合玩家
---@field public discard_pile integer[] @ 弃牌堆
---@field public status_skills Skill[] @ 状态技总和
---@field public observing boolean
Client = class('Client')

View File

@ -252,9 +252,11 @@ function Engine:addGenerals(generals)
end
end
local function canUseGeneral(g)
local r = Fk:currentRoom()
local general = Fk.generals[g]
--- 判断一个武将是否在本房间可用。
---@param g string @ 武将名
function Engine:canUseGeneral(g)
local r = self:currentRoom()
local general = self.generals[g]
if not general then return false end
return not table.contains(r.disabled_packs, general.package.name) and
not table.contains(r.disabled_generals, g) and not general.hidden and not general.total_hidden
@ -270,7 +272,7 @@ function Engine:getSameGenerals(name)
local tName = tmp[#tmp]
local ret = self.same_generals[tName] or {}
return table.filter(ret, function(g)
return g ~= name and self.generals[g] ~= nil and canUseGeneral(g)
return g ~= name and self.generals[g] ~= nil and self:canUseGeneral(g)
end)
end
@ -393,7 +395,7 @@ function Engine:getAllGenerals(except)
local result = {}
for _, general in pairs(self.generals) do
if not (except and table.contains(except, general)) then
if canUseGeneral(general.name) then
if self:canUseGeneral(general.name) then
table.insert(result, general)
end
end

View File

@ -8,8 +8,8 @@
---@field public name string @ 游戏模式名
---@field public minPlayer integer @ 最小玩家数
---@field public maxPlayer integer @ 最大玩家数
---@field public rule TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件)
---@field public logic fun() @ 逻辑通过function完成通常用来初始化、分配身份及座次
---@field public rule nil|TriggerSkill @ 规则(通过技能完成,通常用来为特定角色及特定时机提供触发事件)
---@field public logic nil|fun() @ 逻辑通过function完成通常用来初始化、分配身份及座次
---@field public whitelist string[]|nil @ 白名单
---@field public blacklist string[]|nil @ 黑名单
local GameMode = class("GameMode")

View File

@ -508,7 +508,7 @@ function Player:distanceTo(other, mode, ignore_dead)
temp = temp.next
end
if temp ~= other then
print("Distance malfunction: start and end does not matched.")
print("Distance malfunction: start and end does not match.")
end
local left = #(ignore_dead and Fk:currentRoom().players or Fk:currentRoom().alive_players) - right - #table.filter(Fk:currentRoom().alive_players, function(p) return p:isRemoved() end)
local ret = 0

View File

@ -129,3 +129,7 @@ fk.GeneralRevealed = 89
fk.GeneralHidden = 90
fk.NumOfEvents = 91
fk.BeforePropertyChange = 92
fk.PropertyChange = 93
fk.AfterPropertyChange = 94

View File

@ -294,7 +294,7 @@ GameEvent.functions[GameEvent.Phase] = function(self)
n = 2
}
room.logic:trigger(fk.DrawNCards, player, data)
room:drawCards(player, data.n, self.name)
room:drawCards(player, data.n, "game_rule")
room.logic:trigger(fk.AfterDrawNCards, player, data)
end,
[Player.Play] = function()

View File

@ -9,6 +9,9 @@ GameEvent.functions[GameEvent.ChangeProperty] = function(self)
data.sendLog = data.sendLog or false
local skills = {}
if logic:trigger(fk.PropertyChange, player, data) then
logic:breakEvent()
end
if data.general and data.general ~= "" and data.general ~= player.general then
local originalGeneral = Fk.generals[player.general] or Fk.generals["blank_shibing"]

View File

@ -90,32 +90,8 @@ function GameLogic:chooseGenerals()
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 generals = room:getNGenerals(generalNum)
lord_generals = room:askForGeneral(lord, generals, n)
local lord_general, deputy
if type(lord_generals) == "table" then
deputy = lord_generals[2]
@ -125,6 +101,9 @@ function GameLogic:chooseGenerals()
lord_generals = {lord_general}
end
generals = table.filter(generals, function(g) return not table.contains(lord_generals, g) end)
room:returnToGeneralPile(generals)
room:setPlayerGeneral(lord, lord_general, true)
room:askForChooseKingdom({lord})
room:broadcastProperty(lord, "general")
@ -134,13 +113,10 @@ function GameLogic:chooseGenerals()
end
local nonlord = room:getOtherPlayers(lord, true)
local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals)
local generals = room:getNGenerals(#nonlord * generalNum)
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
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)
end
@ -148,11 +124,13 @@ function GameLogic:chooseGenerals()
room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord)
local selected = {}
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]
local general_ret = json.decode(p.client_reply)
local general = general_ret[1]
local deputy = general_ret[2]
table.insertTableIfNeed(selected, general_ret)
room:setPlayerGeneral(p, general, true, true)
room:setDeputyGeneral(p, deputy)
else
@ -162,6 +140,9 @@ function GameLogic:chooseGenerals()
p.default_reply = ""
end
generals = table.filter(generals, function(g) return not table.contains(selected, g) end)
room:returnToGeneralPile(generals)
room:askForChooseKingdom(nonlord)
end

View File

@ -15,6 +15,7 @@
---@field public game_finished boolean @ 游戏是否已经结束
---@field public timeout integer @ 出牌时长上限
---@field public tag table<string, any> @ Tag清单其实跟Player的标记是差不多的东西
---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组
---@field public draw_pile integer[] @ 摸牌堆这是卡牌id的数组
---@field public discard_pile integer[] @ 弃牌堆也是卡牌id的数组
---@field public processing_area integer[] @ 处理区依然是卡牌id数组
@ -76,6 +77,7 @@ function Room:initialize(_room)
self.game_finished = false
self.timeout = _room:getTimeout()
self.tag = {}
self.general_pile = {}
self.draw_pile = {}
self.discard_pile = {}
self.processing_area = {}
@ -108,7 +110,7 @@ function Room:resume()
-- 如果还没运行的话就先创建自己的主协程
if not self.main_co then
self.main_co = coroutine.create(function()
self.tag["_general_pile"] = Fk:getAllGenerals()
self:makeGeneralPile()
self:run()
end)
end
@ -144,6 +146,26 @@ function Room:resume()
return true
end
-- 构造武将牌堆
function Room:makeGeneralPile()
local trueNames = {}
local ret = {}
if self.game_started then
for _, player in ipairs(self.players) do
trueNames[Fk.generals[player.general].trueName] = true
end
end
for name, general in pairs(Fk.generals) do
if Fk:canUseGeneral(name) and not trueNames[general.trueName] then
table.insert(ret, name)
trueNames[general.trueName] = true
end
end
table.shuffle(ret)
self.general_pile = ret
return true
end
-- 供调度器使用的函数,用来指示房间是否就绪。
-- 如果没有就绪的话,可能会返回第二个值来告诉调度器自己还有多久就绪。
function Room:isReady()
@ -1241,6 +1263,10 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel
if includeEquip then
table.insertTable(hands, player:getCardIds(Player.Equip))
end
local exp = Exppattern:Parse(pattern)
hands = table.filter(hands, function(cid)
return exp:match(Fk:getCardById(cid))
end)
for _ = 1, minNum do
local randomId = hands[math.random(1, #hands)]
table.insert(chosenCards, randomId)
@ -1296,6 +1322,79 @@ function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, patter
end
end
--- 抽个武将
---
--- 同getNCards抽出来就没有了所以记得放回去。
---@param n number @ 数量
---@param position string|nil @位置top/bottom默认top
---@return string[] @ 武将名数组
function Room:getNGenerals(n, position)
position = position or "top"
assert(position == "top" or position == "bottom")
local generals = {}
while n > 0 do
local index = position == "top" and 1 or #self.general_pile
table.insert(generals, table.remove(self.general_pile, index))
n = n - 1
end
if #generals < 1 then
self:gameOver("")
end
return generals
end
--- 把武将牌塞回去(……)
---@param g string[] @ 武将名数组
---@param position string|nil @位置top/bottom默认bottom
---@return boolean @ 是否成功
function Room:returnToGeneralPile(g, position)
position = position or "bottom"
assert(position == "top" or position == "bottom")
if position == "bottom" then
table.insertTable(self.general_pile, g)
elseif position == "top" then
while #g > 0 do
table.insert(self.general_pile, 1, table.remove(g))
end
end
return true
end
--- 抽特定名字的武将(抽了就没了)
---@param name string @ 武将name如找不到则查找truename再找不到则返回nil
---@return string | nil @ 抽出的武将名
function Room:findGeneral(name)
for i, g in ipairs(self.general_pile) do
if g == name or Fk.generals[g].trueName == Fk.generals[name].trueName then
return table.remove(self.general_pile, i)
end
end
return nil
end
--- 自上而下抽符合特定情况的N个武将抽了就没了
---@param func fun(name: string):any @ 武将筛选函数
---@param n integer | nil @ 抽取数量,数量不足则直接抽干净
---@return string[] @ 武将组合,可能为空
function Room:findGenerals(func, n)
n = n or 1
local ret = {}
local index = 1
repeat
if func(self.general_pile[index]) then
table.insert(ret, table.remove(self.general_pile, index))
else
index = index + 1
end
until index >= #self.general_pile or #ret >= n
return ret
end
--- 询问玩家选择一名武将。
---@param player ServerPlayer @ 询问目标
---@param generals string[] @ 可选武将
@ -2078,7 +2177,12 @@ function Room:askForMoveCardInBoard(player, targetOne, targetTwo, skillName, fla
result = json.decode(result)
end
local from, to = result.pos == 0 and targetOne, targetTwo or targetTwo, targetOne
local from, to
if result.pos == 0 then
from, to = targetOne, targetTwo
else
from, to = targetTwo, targetOne
end
local cardToMove = self:getCardOwner(result.cardId):getVirualEquip(result.cardId) or Fk:getCardById(result.cardId)
self:moveCardTo(
cardToMove,
@ -2270,7 +2374,7 @@ function Room:doCardUseEffect(cardUseEvent)
local realCardIds = self:getSubcardsByRule(cardUseEvent.card, { Card.Processing })
self.logic:trigger(fk.BeforeCardUseEffect, cardUseEvent.from, cardUseEvent)
self.logic:trigger(fk.BeforeCardUseEffect, self:getPlayerById(cardUseEvent.from), cardUseEvent)
-- If using Equip or Delayed trick, move them to the area and return
if cardUseEvent.card.type == Card.TypeEquip then
if #realCardIds == 0 then
@ -2919,7 +3023,8 @@ function Room:retrial(card, player, judge, skillName, exchange)
arg = skillName,
}
self:moveCards(move1, move2)
self:moveCards(move1)
self:moveCards(move2)
if triggerResponded then
self.logic:trigger(fk.CardRespondFinished, player, resp)

View File

@ -743,6 +743,7 @@ function ServerPlayer:setChainState(chained)
end
function ServerPlayer:reset()
if self.faceup and not self.chained then return end
self.room:sendLog{
type = "#ChainStateChange",
from = self.id,

View File

@ -1164,10 +1164,88 @@ local diaochan = General:new(extension, "diaochan", "qun", 3, 3, General.Female)
diaochan:addSkill(lijian)
diaochan:addSkill(biyue)
local role_getlogic = function()
local role_logic = GameLogic:subclass("role_logic")
function role_logic:chooseGenerals()
local room = self.room ---@class Room
local generalNum = room.settings.generalNum
local n = room.settings.enableDeputy and 2 or 1
local lord = room:getLord()
local lord_generals = {}
local lord_num = 3
if lord ~= nil then
room.current = lord
local generals = table.connect(room:findGenerals(function(g)
return table.find(Fk.generals[g].skills, function(s) return s.lordSkill end)
end, lord_num), room:getNGenerals(generalNum))
if #room.general_pile < (#room.players - 1) * generalNum then
room:gameOver("")
end
lord_generals = room:askForGeneral(lord, generals, 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
generals = table.filter(generals, function(g) return not table.contains(lord_generals, g) end)
room:returnToGeneralPile(generals)
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 = room:getNGenerals(#nonlord * generalNum)
table.shuffle(generals)
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)
end
room:notifyMoveFocus(nonlord, "AskForGeneral")
room:doBroadcastRequest("AskForGeneral", nonlord)
local selected = {}
for _, p in ipairs(nonlord) do
if p.general == "" and p.reply_ready then
local general_ret = json.decode(p.client_reply)
local general = general_ret[1]
local deputy = general_ret[2]
table.insertTableIfNeed(selected, general_ret)
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
generals = table.filter(generals, function(g) return not table.contains(selected, g) end)
room:returnToGeneralPile(generals)
room:askForChooseKingdom(nonlord)
end
return role_logic
end
local role_mode = fk.CreateGameMode{
name = "aaa_role_mode", -- just to let it at the top of list
minPlayer = 2,
maxPlayer = 8,
logic = role_getlogic,
is_counted = function(self, room)
return #room.players >= 5
end,

View File

@ -295,12 +295,14 @@ local change_hero = fk.CreateActiveSkill{
local from = room:getPlayerById(effect.from)
local target = room:getPlayerById(effect.tos[1])
local choice = self.interaction.data
local generals = table.map(Fk:getGeneralsRandomly(8, Fk:getAllGenerals()), function(p) return p.name end)
local generals = room:getNGenerals(8)
local general = room:askForGeneral(from, generals, 1)
if general == nil then
general = table.random(generals)
end
table.removeOne(generals, general)
room:changeHero(target, general, false, choice == "deputyGeneral", true)
room:returnToGeneralPile(generals)
end,
}
local test_zhenggong = fk.CreateTriggerSkill{