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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds a gate for a list of patterns that pre-filters lines before expensive regex matching.

Extracts the longest literal substring from each pattern using longest_literal. Patterns whose literal is at least MIN_LITERAL (4) characters long are added to a Regexp.union gate; shorter or literal-free patterns are returned in the always_scan list and must be tried on every line.

Examples:

gate, always_scan = build([/lightning/i, /\\d+ damage/])
gate.match?("You cast lightning") #=> MatchData
always_scan.empty? #=> false (short literal)

Parameters:

  • patterns (Array<Regexp>)

    the patterns to gate

Returns:

  • (Array)

    a two-element array: [union_regex, always_scan] where union_regex is a frozen Regexp (or nil if no literals met the threshold) and always_scan is a frozen Array of patterns that bypassed the gate



71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 71

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).



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 33

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 gated pattern (no literal present and no ungated patterns exist).

Returns:

  • (Boolean)


87
88
89
# File 'documented/gemstone/combat/defs/pattern_gate.rb', line 87

def rejects?(gate, always_scan, line)
  always_scan.empty? && (gate.nil? || !gate.match?(line))
end