Module: Lich::Gemstone::Combat::Definitions::PatternGate

Defined in:
documented/gemstone/combat/defs/pattern_gate.rb

Constant Summary collapse

MIN_LITERAL =

Build a gate for a list of patterns. Returns [union_regex, always_scan] where union_regex matches iff some pattern's literal is present, and always_scan lists patterns whose literal was too short to be a useful gate (they must be tried on every line).

4

Class Method Summary collapse

Class Method Details

.build(patterns) ⇒ Array(Regexp, nil, Array<Regexp>)

Builds a literal-substring gate for a list of patterns to accelerate pattern matching.

Extracts the longest guaranteed-literal run from each pattern using longest_literal, then creates a fast Regexp.union gate that rejects lines lacking those substrings before attempting full pattern matches. Patterns with literals shorter than MIN_LITERAL (4 chars) are placed in the always_scan list and must be tried on every line. Returns a tuple that rejects? uses to make reject-or-scan decisions.

Examples:

gate, always_scan = PatternGate.build([/\bwounds\s+(\.+)/, /^You \w+ a/])
gate.match?("serious wounds...") #=> #<MatchData "wounds">
always_scan.length #=> 0 (both literals met threshold)

Parameters:

  • patterns (Array<Regexp>) —

    regex patterns to gate

Returns:

  • (Array(Regexp, nil, Array<Regexp>)) —

    tuple of [union_regex_or_nil, always_scan_patterns]; union_regex is frozen and matches iff some pattern's literal is present (nil if no patterns have literals >= MIN_LITERAL); always_scan is a frozen array of patterns that must be tried on every candidate line



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 84

def build(patterns)
  literals = []
  always_scan = []
  patterns.each do |pattern|
    literal = longest_literal(pattern)
    if literal && literal.length >= MIN_LITERAL
      literals << literal
    else
      always_scan << pattern
    end
  end
  [literals.empty? ? nil : Regexp.union(literals.uniq).freeze, always_scan.freeze]
end

.longest_literal(regex) ⇒ Object

Longest guaranteed-literal run in a regex source, or nil when no safe literal exists. Character classes, escapes and then entire parenthesized groups (innermost-out, so nesting works) are removed wholesale - text inside a group may be optional or one alternation branch, so it is never guaranteed. What survives is top-level text that every match must contain; the longest metachar-free fragment of it is the gate literal. A source with a top-level | is a pure alternation with no guaranteed text - returns nil (always scan).



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 45

def longest_literal(regex)
  source = regex.source.dup
  source.gsub!(/\\[A-Za-z]/, "\x00")        # escape sequences (\d, \w, \b...)
  source.gsub!(/\[[^\]]*\]/, "\x00")        # character classes
  # Remove groups innermost-first so nested groups collapse cleanly
  nil while source.gsub!(/\((?:\?(?:<[a-zA-Z_]+>|:|=|!))?[^()]*\)/, "\x00")
  return nil if source.include?('|') # top-level alternation
  fragments = source.split(/[\\(){}?*+.^$\x00]/)
  # A fragment followed by ? or * in the original is optional; the
  # split above already breaks on those metachars, but the char
  # BEFORE ? belongs to the fragment - trim it to stay conservative.
  longest = fragments.max_by(&:length).to_s
  longest = longest[0..-2] if source =~ /#{Regexp.escape(longest)}[?*]/
  longest.empty? ? nil : longest
end

.rejects?(gate, always_scan, line) ⇒ Boolean

Convenience: true when the line can't possibly match any pattern in this table, so the caller may skip the full scan. A line is only rejectable when BOTH the literal gate misses AND no ungated (always_scan) pattern matches. Any always_scan pattern that matches keeps the line in play; a non-empty always_scan does NOT blanket- disable rejection (that was the old bug - it reverted the whole table to full-scan the moment one short-literal pattern existed).

Returns:

  • (Boolean)


105
106
107
108
109
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 105

def rejects?(gate, always_scan, line)
  return false if gate&.match?(line)

  always_scan.none? { |rx| rx.match?(line) }
end