433 lines
10 KiB
Lua
433 lines
10 KiB
Lua
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
--[[
|
||
|
||
Exppattern 是一个用来描述卡牌的字符串。
|
||
pattern 字符串会被用来构建新的 Exppattern 对象,然后可以用它来检查一张牌。
|
||
|
||
pattern 字符串的语法:
|
||
1. 整个字符串可以被分号 (';') 切割,每一个分割就是一个 Matcher
|
||
2. 对于 Matcher 字符串,它是用 ('|') 分割的
|
||
3. 然后在 Matcher 的每一个细分中,又可以用 ',' 来进行更进一步的分割
|
||
|
||
其中 Matcher 的格式为 牌名|点数|花色|区域|完整牌名|牌类型|牌id
|
||
更进一步,“点数” 可以用 '~' 符号表示数字的范围,并且可以用 AJQK 表示对应点数
|
||
|
||
例如:
|
||
slash,jink|2~4|spade;.|.|.|.|.|trick
|
||
|
||
你可以使用 '^' 符号表示否定,比如 ^heart 表示除了红桃之外的所有花色。
|
||
否定型一样的可以与其他表达式并用,用 ',' 分割。
|
||
如果要同时否定多项,则需要用括号: ^(heart, club) 等。
|
||
注:这种括号不支持嵌套否定。
|
||
|
||
]]--
|
||
|
||
---@class Matcher
|
||
---@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[]
|
||
|
||
-- v0.2.6改动: cardType会被解析为trueName数组和name数组,而不是自己单独成立
|
||
|
||
local numbertable = {
|
||
["A"] = 1,
|
||
["J"] = 11,
|
||
["Q"] = 12,
|
||
["K"] = 13,
|
||
}
|
||
|
||
local placetable = {
|
||
[Card.PlayerHand] = "hand",
|
||
[Card.PlayerEquip] = "equip",
|
||
}
|
||
|
||
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
|
||
|
||
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()
|
||
-- elseif key == "cardType" then
|
||
-- val = card:getTypeString()
|
||
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
|
||
end
|
||
|
||
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
|
||
else
|
||
if t ~= val then return true end
|
||
end
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
---@param matcher Matcher
|
||
---@param card Card
|
||
local function matchCard(matcher, card)
|
||
if type(card) == "number" then
|
||
card = Fk:getCardById(card)
|
||
end
|
||
|
||
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")
|
||
-- and matchSingleKey(matcher, card, "cardType")
|
||
and matchSingleKey(matcher, card, "id")
|
||
end
|
||
|
||
local function hasNegIntersection(a, b)
|
||
-- 注意,这里是拿a.neg和b比
|
||
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
|
||
-- 没法比
|
||
end
|
||
|
||
local function hasIntersection(a, b)
|
||
if a == nil or b == nil or (#a + #b == 0) then
|
||
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
|
||
local neg_pass = hasNegIntersection(a, b) or hasNegIntersection(b, a)
|
||
|
||
return neg_pass
|
||
end
|
||
|
||
---@param a Matcher
|
||
---@param b Matcher
|
||
local function matchMatcher(a, b)
|
||
local keys = {
|
||
"trueName",
|
||
"number",
|
||
"suit",
|
||
"place",
|
||
"name",
|
||
-- "cardType",
|
||
"id",
|
||
}
|
||
|
||
for _, k in ipairs(keys) do
|
||
if not hasIntersection(a[k], b[k]) then
|
||
return false
|
||
end
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
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
|
||
|
||
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
|
||
|
||
for _, list in ipairs(t) do
|
||
parseNegative(list)
|
||
end
|
||
|
||
local ret = {} ---@type Matcher
|
||
ret.trueName = not table.contains(t[1], ".") and t[1] or nil
|
||
|
||
if not table.contains(t[2], ".") then
|
||
ret.number = parseRawNumTable(t[2])
|
||
end
|
||
|
||
ret.suit = not table.contains(t[3], ".") and t[3] or nil
|
||
ret.place = not table.contains(t[4], ".") and t[4] or nil
|
||
ret.name = not table.contains(t[5], ".") and t[5] or nil
|
||
-- 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
|
||
|
||
if not table.contains(t[7], ".") then
|
||
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
|
||
end
|
||
end
|
||
end
|
||
return ret
|
||
end
|
||
|
||
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
|
||
|
||
---@class Exppattern: Object
|
||
---@field public matchers Matcher[]
|
||
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
|
||
|
||
---@param pattern string
|
||
---@return Exppattern
|
||
function Exppattern:Parse(pattern)
|
||
error("This is a static method. Please use Exppattern:Parse instead")
|
||
end
|
||
|
||
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
|
||
|
||
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
|
||
|
||
return Exppattern
|