2023-04-09 05:35:35 +00:00
|
|
|
|
-- SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
--[[
|
|
|
|
|
|
2023-04-12 12:51:09 +00:00
|
|
|
|
Exppattern 是一个用来描述卡牌的字符串。
|
|
|
|
|
pattern 字符串会被用来构建新的 Exppattern 对象,然后可以用它来检查一张牌。
|
2023-01-16 11:13:07 +00:00
|
|
|
|
|
2023-04-12 12:51:09 +00:00
|
|
|
|
pattern 字符串的语法:
|
|
|
|
|
1. 整个字符串可以被分号 (';') 切割,每一个分割就是一个 Matcher
|
|
|
|
|
2. 对于 Matcher 字符串,它是用 ('|') 分割的
|
|
|
|
|
3. 然后在 Matcher 的每一个细分中,又可以用 ',' 来进行更进一步的分割
|
2023-01-16 11:13:07 +00:00
|
|
|
|
|
2024-01-24 19:13:57 +00:00
|
|
|
|
其中 Matcher 的格式为 牌名|点数|花色|区域|完整牌名|牌类型|牌id
|
2023-04-12 12:51:09 +00:00
|
|
|
|
更进一步,“点数” 可以用 '~' 符号表示数字的范围,并且可以用 AJQK 表示对应点数
|
2023-01-16 11:13:07 +00:00
|
|
|
|
|
2023-04-12 12:51:09 +00:00
|
|
|
|
例如:
|
2023-01-16 11:13:07 +00:00
|
|
|
|
slash,jink|2~4|spade;.|.|.|.|.|trick
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
你可以使用 '^' 符号表示否定,比如 ^heart 表示除了红桃之外的所有花色。
|
|
|
|
|
否定型一样的可以与其他表达式并用,用 ',' 分割。
|
|
|
|
|
如果要同时否定多项,则需要用括号: ^(heart, club) 等。
|
|
|
|
|
注:这种括号不支持嵌套否定。
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
]]--
|
|
|
|
|
|
|
|
|
|
---@class Matcher
|
2024-04-19 12:53:19 +00:00
|
|
|
|
---@field public trueName? string[]
|
|
|
|
|
---@field public number? integer[]
|
|
|
|
|
---@field public suit? string[]
|
|
|
|
|
---@field public place? string[]
|
|
|
|
|
---@field public name? string[]
|
|
|
|
|
---@field public cardType? string[]
|
|
|
|
|
---@field public id? integer[]
|
2023-01-16 11:13:07 +00:00
|
|
|
|
|
2023-06-24 04:29:20 +00:00
|
|
|
|
-- v0.2.6改动: cardType会被解析为trueName数组和name数组,而不是自己单独成立
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
local numbertable = {
|
|
|
|
|
["A"] = 1,
|
|
|
|
|
["J"] = 11,
|
|
|
|
|
["Q"] = 12,
|
|
|
|
|
["K"] = 13,
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-29 10:11:41 +00:00
|
|
|
|
local placetable = {
|
|
|
|
|
[Card.PlayerHand] = "hand",
|
|
|
|
|
[Card.PlayerEquip] = "equip",
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-24 04:29:20 +00:00
|
|
|
|
local card_type_table = {}
|
|
|
|
|
|
|
|
|
|
local function fillCardTypeTable()
|
|
|
|
|
local tmp = {}
|
|
|
|
|
for _, cd in ipairs(Fk.cards) do
|
|
|
|
|
local t = cd:getTypeString()
|
|
|
|
|
local st = cd:getSubtypeString()
|
|
|
|
|
local tn = cd.trueName
|
|
|
|
|
-- TODO: local n = cd.name
|
|
|
|
|
|
|
|
|
|
if not tmp[tn] then
|
|
|
|
|
card_type_table[t] = card_type_table[t] or {}
|
|
|
|
|
card_type_table[st] = card_type_table[st] or {}
|
|
|
|
|
table.insertIfNeed(card_type_table[t], tn)
|
|
|
|
|
table.insertIfNeed(card_type_table[st], tn)
|
|
|
|
|
tmp[tn] = true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
local function matchSingleKey(matcher, card, key)
|
|
|
|
|
local match = matcher[key]
|
|
|
|
|
if not match then return true end
|
|
|
|
|
local val = card[key]
|
|
|
|
|
if key == "suit" then
|
|
|
|
|
val = card:getSuitString()
|
2023-06-24 04:29:20 +00:00
|
|
|
|
-- elseif key == "cardType" then
|
|
|
|
|
-- val = card:getTypeString()
|
2023-04-20 20:29:52 +00:00
|
|
|
|
elseif key == "place" then
|
|
|
|
|
val = placetable[Fk:currentRoom():getCardArea(card.id)]
|
|
|
|
|
if not val then
|
|
|
|
|
for _, p in ipairs(Fk:currentRoom().alive_players) do
|
|
|
|
|
val = p:getPileNameOfId(card.id)
|
|
|
|
|
if val then break end
|
|
|
|
|
end
|
|
|
|
|
end
|
2023-01-16 11:13:07 +00:00
|
|
|
|
end
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
if table.contains(match, val) then
|
|
|
|
|
return true
|
|
|
|
|
else
|
|
|
|
|
local neg = match.neg
|
|
|
|
|
if not neg then return false end
|
|
|
|
|
for _, t in ipairs(neg) do
|
|
|
|
|
if type(t) == "table" then
|
|
|
|
|
if not table.contains(t, val) then return true end
|
2023-03-07 02:21:56 +00:00
|
|
|
|
else
|
2023-04-20 20:29:52 +00:00
|
|
|
|
if t ~= val then return true end
|
2023-03-04 17:28:59 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2023-01-16 11:13:07 +00:00
|
|
|
|
end
|
2023-04-20 20:29:52 +00:00
|
|
|
|
return false
|
|
|
|
|
end
|
2023-02-26 08:51:29 +00:00
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
---@param matcher Matcher
|
|
|
|
|
---@param card Card
|
|
|
|
|
local function matchCard(matcher, card)
|
|
|
|
|
if type(card) == "number" then
|
|
|
|
|
card = Fk:getCardById(card)
|
2023-01-16 11:13:07 +00:00
|
|
|
|
end
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
return matchSingleKey(matcher, card, "trueName")
|
|
|
|
|
and matchSingleKey(matcher, card, "number")
|
|
|
|
|
and matchSingleKey(matcher, card, "suit")
|
|
|
|
|
and matchSingleKey(matcher, card, "place")
|
|
|
|
|
and matchSingleKey(matcher, card, "name")
|
2023-06-24 04:29:20 +00:00
|
|
|
|
-- and matchSingleKey(matcher, card, "cardType")
|
2023-04-20 20:29:52 +00:00
|
|
|
|
and matchSingleKey(matcher, card, "id")
|
2023-01-16 11:13:07 +00:00
|
|
|
|
end
|
|
|
|
|
|
2023-06-24 04:29:20 +00:00
|
|
|
|
local function hasNegIntersection(a, b)
|
2023-07-11 15:16:46 +00:00
|
|
|
|
-- 注意,这里是拿a.neg和b比
|
2023-06-24 04:29:20 +00:00
|
|
|
|
local neg_pass = false
|
|
|
|
|
|
|
|
|
|
-- 第一次比较: 比较neg和正常值,如有不同即认为可以匹配
|
|
|
|
|
-- 比如 ^jink 可以匹配 slash,jink
|
|
|
|
|
for _, neg in ipairs(a.neg or Util.DummyTable) do
|
|
|
|
|
for _, e in ipairs(b) do
|
|
|
|
|
if type(neg) == "table" then
|
|
|
|
|
neg_pass = not table.contains(neg, e)
|
|
|
|
|
else
|
|
|
|
|
neg_pass = neg ~= e
|
|
|
|
|
end
|
|
|
|
|
if neg_pass then return true end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- 第二次比较: 比较双方neg
|
|
|
|
|
-- 比如 ^jink 可以匹配 ^slash
|
2023-07-11 15:16:46 +00:00
|
|
|
|
-- 没法比
|
2023-06-24 04:29:20 +00:00
|
|
|
|
end
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
local function hasIntersection(a, b)
|
2023-07-11 15:16:46 +00:00
|
|
|
|
if a == nil or b == nil or (#a + #b == 0) then
|
2023-01-16 11:13:07 +00:00
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local tmp = {}
|
|
|
|
|
for _, e in ipairs(a) do
|
|
|
|
|
tmp[e] = true
|
|
|
|
|
end
|
|
|
|
|
for _, e in ipairs(b) do
|
|
|
|
|
if tmp[e] then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
2023-06-24 04:29:20 +00:00
|
|
|
|
local neg_pass = hasNegIntersection(a, b) or hasNegIntersection(b, a)
|
2023-04-20 20:29:52 +00:00
|
|
|
|
|
2023-06-24 04:29:20 +00:00
|
|
|
|
return neg_pass
|
2023-01-16 11:13:07 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---@param a Matcher
|
|
|
|
|
---@param b Matcher
|
|
|
|
|
local function matchMatcher(a, b)
|
|
|
|
|
local keys = {
|
2023-04-20 20:29:52 +00:00
|
|
|
|
"trueName",
|
2023-01-16 11:13:07 +00:00
|
|
|
|
"number",
|
|
|
|
|
"suit",
|
|
|
|
|
"place",
|
2023-04-20 20:29:52 +00:00
|
|
|
|
"name",
|
2023-06-24 04:29:20 +00:00
|
|
|
|
-- "cardType",
|
2023-01-16 11:13:07 +00:00
|
|
|
|
"id",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, k in ipairs(keys) do
|
|
|
|
|
if not hasIntersection(a[k], b[k]) then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
local function parseNegative(list)
|
|
|
|
|
local bracket = nil
|
|
|
|
|
local toRemove = {}
|
|
|
|
|
for i, element in ipairs(list) do
|
|
|
|
|
if element[1] == "^" or bracket then
|
|
|
|
|
list.neg = list.neg or {}
|
|
|
|
|
table.insert(toRemove, 1, i)
|
|
|
|
|
if element[1] == "^" and element[2] == "(" then
|
|
|
|
|
if bracket then
|
|
|
|
|
error("pattern syntax error. Cannot use nested bracket.")
|
|
|
|
|
else
|
|
|
|
|
bracket = {}
|
|
|
|
|
end
|
|
|
|
|
element = element:sub(3)
|
|
|
|
|
else
|
|
|
|
|
if element[1] == "^" then
|
|
|
|
|
element = element:sub(2)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local eofBracket
|
|
|
|
|
if element:endsWith(")") then
|
|
|
|
|
eofBracket = true
|
|
|
|
|
element = element:sub(1, -2)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if eofBracket then
|
|
|
|
|
if not bracket then
|
|
|
|
|
error('pattern syntax error. No matching bracket.')
|
|
|
|
|
else
|
|
|
|
|
table.insert(bracket, element)
|
|
|
|
|
table.insert(list.neg, bracket)
|
|
|
|
|
bracket = nil
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
if bracket then
|
|
|
|
|
table.insert(bracket, element)
|
|
|
|
|
else
|
|
|
|
|
table.insert(list.neg, element)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for _, i in ipairs(toRemove) do
|
|
|
|
|
table.remove(list, i)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function parseNumToTable(from, dest)
|
|
|
|
|
for _, num in ipairs(from) do
|
|
|
|
|
if type(num) ~= "string" then goto continue end
|
|
|
|
|
local n = tonumber(num)
|
|
|
|
|
if not n then
|
|
|
|
|
n = numbertable[num]
|
|
|
|
|
end
|
|
|
|
|
if n then
|
|
|
|
|
table.insertIfNeed(dest, n)
|
|
|
|
|
else
|
|
|
|
|
if string.find(num, "~") then
|
|
|
|
|
local s, e = table.unpack(num:split("~"))
|
|
|
|
|
local start = tonumber(s)
|
|
|
|
|
if not start then
|
|
|
|
|
start = numbertable[s]
|
|
|
|
|
end
|
|
|
|
|
local _end = tonumber(e)
|
|
|
|
|
if not _end then
|
|
|
|
|
_end = numbertable[e]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i = start, _end do
|
|
|
|
|
table.insertIfNeed(dest, i)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
::continue::
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function parseRawNumTable(tab)
|
|
|
|
|
local ret = {}
|
|
|
|
|
parseNumToTable(tab, ret)
|
|
|
|
|
|
|
|
|
|
if tab.neg then
|
|
|
|
|
ret.neg = {}
|
|
|
|
|
parseNumToTable(tab.neg, ret.neg)
|
|
|
|
|
|
|
|
|
|
for _, t in ipairs(tab.neg) do
|
|
|
|
|
if type(t) == "table" then
|
|
|
|
|
local tmp = {}
|
|
|
|
|
parseNumToTable(t, tmp)
|
|
|
|
|
table.insert(ret.neg, tmp)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return ret
|
|
|
|
|
end
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
local function parseMatcher(str)
|
|
|
|
|
local t = str:split("|")
|
|
|
|
|
if #t < 7 then
|
|
|
|
|
for i = 1, 7 - #t do
|
|
|
|
|
table.insert(t, ".")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i, item in ipairs(t) do
|
|
|
|
|
t[i] = item:split(",")
|
|
|
|
|
end
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
for _, list in ipairs(t) do
|
|
|
|
|
parseNegative(list)
|
|
|
|
|
end
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
local ret = {} ---@type Matcher
|
2023-04-20 20:29:52 +00:00
|
|
|
|
ret.trueName = not table.contains(t[1], ".") and t[1] or nil
|
2023-01-16 11:13:07 +00:00
|
|
|
|
|
|
|
|
|
if not table.contains(t[2], ".") then
|
2023-04-20 20:29:52 +00:00
|
|
|
|
ret.number = parseRawNumTable(t[2])
|
2023-01-16 11:13:07 +00:00
|
|
|
|
end
|
|
|
|
|
|
2023-01-29 10:11:41 +00:00
|
|
|
|
ret.suit = not table.contains(t[3], ".") and t[3] or nil
|
2023-03-07 02:21:56 +00:00
|
|
|
|
ret.place = not table.contains(t[4], ".") and t[4] or nil
|
2023-04-20 20:29:52 +00:00
|
|
|
|
ret.name = not table.contains(t[5], ".") and t[5] or nil
|
2023-06-24 04:29:20 +00:00
|
|
|
|
-- ret.cardType = not table.contains(t[6], ".") and t[6] or nil
|
|
|
|
|
if table.empty(card_type_table) then
|
|
|
|
|
fillCardTypeTable()
|
|
|
|
|
end
|
|
|
|
|
for _, ctype in ipairs(t[6]) do
|
|
|
|
|
for _, n in ipairs(card_type_table[ctype] or Util.DummyTable) do
|
|
|
|
|
if not ret.trueName then ret.trueName = {} end
|
|
|
|
|
table.insertIfNeed(ret.trueName, n)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
for _, neg in ipairs(t[6].neg or Util.DummyTable) do
|
|
|
|
|
if type(neg) ~= "table" then neg = { neg } end
|
|
|
|
|
if not ret.trueName then ret.trueName = {} end
|
|
|
|
|
if not ret.trueName.neg then ret.trueName.neg = {} end
|
|
|
|
|
|
|
|
|
|
local temp = {}
|
|
|
|
|
for _, ctype in ipairs(neg) do
|
|
|
|
|
table.insertTable(temp, card_type_table[ctype] or Util.DummyTable)
|
|
|
|
|
end
|
|
|
|
|
table.insert(ret.trueName.neg, temp)
|
|
|
|
|
end
|
2023-01-16 11:13:07 +00:00
|
|
|
|
|
|
|
|
|
if not table.contains(t[7], ".") then
|
2023-04-20 20:29:52 +00:00
|
|
|
|
ret.id = parseRawNumTable(t[7])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function matcherKeyToString(tab)
|
|
|
|
|
if not tab then return "." end
|
|
|
|
|
local ret = table.concat(tab, ",")
|
|
|
|
|
if tab.neg then
|
|
|
|
|
for _, t in ipairs(tab.neg) do
|
|
|
|
|
if ret ~= "" then ret = ret .. "," end
|
|
|
|
|
if type(t) == "table" then
|
|
|
|
|
ret = ret .. ("^(" .. table.concat(t, ",") .. ")")
|
|
|
|
|
else
|
|
|
|
|
ret = ret .. "^" .. t
|
2023-01-16 11:13:07 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return ret
|
|
|
|
|
end
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
local function matcherToString(matcher)
|
|
|
|
|
return table.concat({
|
|
|
|
|
matcherKeyToString(matcher.trueName),
|
|
|
|
|
matcherKeyToString(matcher.number),
|
|
|
|
|
matcherKeyToString(matcher.suit),
|
|
|
|
|
matcherKeyToString(matcher.place),
|
|
|
|
|
matcherKeyToString(matcher.name),
|
|
|
|
|
matcherKeyToString(matcher.cardType),
|
|
|
|
|
matcherKeyToString(matcher.id),
|
|
|
|
|
}, "|")
|
|
|
|
|
end
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
---@class Exppattern: Object
|
2023-03-26 09:32:45 +00:00
|
|
|
|
---@field public matchers Matcher[]
|
2023-01-16 11:13:07 +00:00
|
|
|
|
local Exppattern = class("Exppattern")
|
|
|
|
|
|
|
|
|
|
function Exppattern:initialize(spec)
|
|
|
|
|
if not spec then
|
|
|
|
|
self.matchers = {}
|
|
|
|
|
elseif spec[1] ~= nil then
|
|
|
|
|
self.matchers = spec
|
|
|
|
|
else
|
|
|
|
|
self.matchers = {}
|
|
|
|
|
self.matchers[1] = spec
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2023-03-26 09:32:45 +00:00
|
|
|
|
---@param pattern string
|
|
|
|
|
---@return Exppattern
|
|
|
|
|
function Exppattern:Parse(pattern)
|
|
|
|
|
error("This is a static method. Please use Exppattern:Parse instead")
|
|
|
|
|
end
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
function Exppattern.static:Parse(str)
|
|
|
|
|
local ret = Exppattern:new()
|
|
|
|
|
local t = str:split(";")
|
|
|
|
|
for i, s in ipairs(t) do
|
|
|
|
|
ret.matchers[i] = parseMatcher(s)
|
|
|
|
|
end
|
|
|
|
|
return ret
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---@param card Card
|
|
|
|
|
function Exppattern:match(card)
|
|
|
|
|
for _, matcher in ipairs(self.matchers) do
|
|
|
|
|
local result = matchCard(matcher, card)
|
|
|
|
|
if result then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function Exppattern:matchExp(exp)
|
|
|
|
|
if type(exp) == "string" then
|
|
|
|
|
exp = Exppattern:Parse(exp)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local a = self.matchers
|
|
|
|
|
local b = exp.matchers
|
|
|
|
|
|
|
|
|
|
for _, m in ipairs(a) do
|
|
|
|
|
for _, n in ipairs(b) do
|
|
|
|
|
if matchMatcher(m, n) then
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
2023-04-20 20:29:52 +00:00
|
|
|
|
function Exppattern:__tostring()
|
|
|
|
|
local ret = ""
|
|
|
|
|
for i, matcher in ipairs(self.matchers) do
|
|
|
|
|
if i > 1 then ret = ret .. ";" end
|
|
|
|
|
ret = ret .. matcherToString(matcher)
|
|
|
|
|
end
|
|
|
|
|
return ret
|
|
|
|
|
end
|
|
|
|
|
|
2023-01-16 11:13:07 +00:00
|
|
|
|
return Exppattern
|