Class: Lich::Gemstone::Bounty::Parser

Inherits:
Object
  • Object
show all
Defined in:
documented/gemstone/bounty/parser.rb

Overview

Parses bounty task descriptions from GemStone IV/DragonRealms game server output and extracts structured task data.

Matches task descriptions against a set of regex patterns to determine the task type and extract named captures like creature type, town, item name, and numeric requirements. Handles special cases like creature name normalization and town name ambiguity resolution.

Constant Summary collapse

HMM_REGEX =

Matches the opening phrase "Hmm, I've got a task here from ." in assignment messages, with optional town name capture.

Examples:

"Hmm, I've got a task here from Wehnimer's Landing." #=> matches with town="Wehnimer's Landing"

Returns:

  • (Regexp)

    pattern with named capture group 'town'

See Also:

/(?:Hmm, I've got a task here from .*?(?<town>[A-Z].*?)\..*?)?/
LOCATION_REGEX =

Matches location phrases like "near the Lich Gate" or "in the Graveyard", with named captures for the area and optional nearby town.

Examples:

"on the field near Wehnimer's Landing" #=> matches with area="field", town="Wehnimer's Landing"

Returns:

  • (Regexp)

    pattern with named capture groups 'area' and 'town'

See Also:

/(?:on|in|near) (?:the\s+)?(?<area>[^.]+?)(?:\s+(?:near|between|under) (?<town>[^.]+))?/
GUARD_REGEX =

Matches various NPC guard and guard-like encounter descriptions, including city gate guards, militia, sentries, ship pursers, and tavern keepers.

Examples:

"one of the guardsmen just inside the Ta'Illistim City Gate" #=> matches with town="Ta'Illistim"
"the sentry just outside Kraken's Fall" #=> matches with town="Kraken's Fall"

Returns:

  • (Regexp)

    union of patterns, some with named capture group 'town'

See Also:

Regexp.union(
  /one of the guardsmen just inside the (?<town>Ta'Illistim) City Gate/,
  /one of the guardsmen just inside the Sapphire Gate/,
  /one of the guardsmen just inside the gate/,
  /one of the (?<town>.*) (?:gate|tunnel) guards/,
  /one of the (?<town>Icemule Trace) gate guards or the halfing Belle at the Pinefar Trading Post/,
  /Quin Telaren of (?<town>Wehnimer's Landing)/,
  /the dwarven militia sergeant near the (?<town>Kharam-Dzu) town gates/,
  /the sentry just outside town/,
  /the sentry just outside (?<town>Kraken's Fall)/,
  /the purser of (?<town>River's Rest)/,
  /the tavernkeeper at Rawknuckle's Common House/,
  /the captain of the (?<town>Contempt)/,
  /the elderly guard in the East Guardtower/
)
CONCOCTION_REGEX =

Matches herb task descriptions where an NPC requires pristine herb samples from a specific area, with captures for herb type, area, and quantity.

Examples:

"is working on a concoction that requires some eonake found in the Dragonspine. These samples must be in pristine condition. You have been tasked to retrieve 3 samples." #=> matches with herb="eonake", area="Dragonspine", number="3"

Returns:

  • (Regexp)

    pattern with named capture groups 'herb', 'area', and 'number'

See Also:

/is working on a concoction that requires (?:an?|some|several) (?<herb>[^.]+?) found [oi]n (?:the\s+)?(?<area>[^.]+?)(?:\s+(?:near|under|between) [^.]+)?\.  These samples must be in pristine condition\.  You have been tasked to retrieve (?<number>\d+) (?:more\s+)?samples?\./
TASK_MAYBE_REGEX =

Matches the taskmaster direct-speech opening phrase for task assignment messages.

Examples:

"The taskmaster told you: \"I've got a mission for you.\"" #=> matches

Returns:

  • (Regexp)

    pattern for taskmaster quote prefix

See Also:

/^(?:The taskmaster told you:  ")/
TASK_MATCHERS =

Hash mapping task type symbols to regex patterns that identify and capture details for each task type.

Task types include: :none, :bandit_assignment, :creature_assignment, :gem_assignment, :heirloom_assignment, :herb_assignment, :rescue_assignment, :skin_assignment, :taskmaster, :heirloom_found, :guard, :dangerous_spawned, :rescue_spawned, :bandit, :dangerous, :escort, :gem, :heirloom, :herb, :rescue, :skin, :cull, and :failed.

Returns:

  • (Hash{Symbol => Regexp})

    mapping of task types to their patterns

See Also:

{
  :none                => /^You are not currently assigned a task/,
  :bandit_assignment   => /#{HMM_REGEX}It appears they have a bandit problem they'd like you to solve/,
  :creature_assignment => Regexp.union(
    /#{HMM_REGEX}It appears they have a creature problem they'd like you to solve/,
    /#{TASK_MAYBE_REGEX}I've got an urgent mission for you.  We have a creature problem we'd like you to solve.  Go report to the (?<town>[A-Z].*?) to find out/,
    /#{TASK_MAYBE_REGEX}I've a favor to ask of you.  We have a creature problem we'd like you to solve: you know, by killing.  Go report to the (?<town>[A-Z].*?)/
  ),
  :gem_assignment      => Regexp.union(
    /#{HMM_REGEX}The local gem dealer, (?<npc_name>[^,]+), has an order to fill and wants our help/,
    /All right.  I've a mission for you.  Our guest, the trader (?<npc_name>[^,]+)/,
  ),
  :heirloom_assignment => Regexp.union(
    /#{HMM_REGEX}It appears they need your help in tracking down some kind of lost heirloom/,
    /#{TASK_MAYBE_REGEX}?It's time for you to earn your keep around here.  I'd like you track down a lost heirloom./
  ),
  :herb_assignment     => Regexp.union(
    /#{HMM_REGEX}The local [^,]+?, (?<npc_name>[^,]+), has asked for our aid.  Head over there and see what you can do.  Be sure to ASK about BOUNTIES./,
    /#{TASK_MAYBE_REGEX}I've got a mission for you.  Our [^,]+?, (?<npc_name>[^,]+), has asked for our aid.  Head over there and see what you can do./
  ),
  :rescue_assignment   => /#{HMM_REGEX}It appears that a local resident urgently needs our help in some matter/,
  :skin_assignment     => Regexp.union(
    /#{HMM_REGEX}The local furrier (?<npc_name>.+) has an order to fill and wants our help/,
    /#{TASK_MAYBE_REGEX}?You look like you need work.  The flesh merchant (?<npc_name>.+), down in the hold, has an order to fill and wants our help./
  ),
  :taskmaster          => /^You have succeeded in your task and can return to the Adventurer's Guild/,
  :heirloom_found      => /^You have located (?:an?|some) (?<item>.+) and should bring (it back|your find) to #{GUARD_REGEX}\.$/,
  :guard               => /^You succeeded in your task and should report back to #{GUARD_REGEX}\.$/,
  :dangerous_spawned   => /^You have been tasked to hunt down and kill a particularly dangerous (?<creature>[^.]+) that has established a territory #{LOCATION_REGEX}\.  You have provoked (?:his|her|its) attention and now you must(?: return to where you left (?:him|her|it) and)? kill (?:him|her|it)!$/,
  :rescue_spawned      => /^You have made contact with the child you are to rescue and you must get (?:him|her) back alive to #{GUARD_REGEX}\.$/,
  :bandit              => /^You have been tasked to(?: help (?<assist>\w+))? suppress (?<creature>bandit) activity #{LOCATION_REGEX}\.  You need to kill (?<number>\d+) (?:more\s+)?of them to complete your task\.$/,
  :dangerous           => /^You have been tasked to hunt down and kill a (?:particularly )?dangerous (?<creature>[^.]+) that has established a territory #{LOCATION_REGEX}\.  You can get its attention by killing other creatures of the same type in its territory\.$/,
  :escort              => /#{TASK_MAYBE_REGEX}?I've got a special mission for you\.  A certain client has hired us to provide a protective escort on (?:his|her) upcoming journey\.  Go to (?<start>[^.]+) and WAIT for (?:him|her) to meet you there\.  You must guarantee (?:his|her) safety to (?<destination>[^.]+) as soon as you can, being ready for any dangers that the two of you may face\.  Good luck!"?$/,
  :gem                 => Regexp.union(
    /^The gem dealer in (?<town>[^,]+), (?<npc_name>[^,]+), has received orders from multiple customers requesting (?:an?|some) (?<gem>[^.]+)\.  You have been tasked to retrieve (?<number>\d+) (?:more\s+)?of them\.  You can SELL them to the gem dealer as you find them\.$/,
    /^The gem dealer in has received orders from multiple customers requesting (?:an?|some) (?<gem>[^.]+)\.  You have been tasked to retrieve (?<number>\d+) (?:more\s+)?of them\.  You can SELL them to the gem dealer as you find them\.$/
  ),
  :heirloom            => /^You have been tasked to recover (?:an?|some) (?<item>[^.]+) that an unfortunate citizen lost after being attacked by an? (?<creature>[^.]+?) #{LOCATION_REGEX}\.  The heirloom can be identified by the initials \w+ engraved upon it\.  [^.]*?(?<action>LOOT|SEARCH)[^.]+\.$/,
  :herb                => Regexp.union(
    /^The .+? in (?<town>[^,]+?), (?<npc_name>[^,]+), #{CONCOCTION_REGEX}$/,
    /^The .+, (?<npc_name>[^,]+), aboard the (?<town>\w+?) in .*? #{CONCOCTION_REGEX}$/
  ),
  :rescue              => /^You have been tasked to rescue the young (?:runaway|kidnapped) (?:son|daughter) of a local citizen\.  A local divinist has had visions of the child fleeing from an? (?<creature>[^.]+?) #{LOCATION_REGEX}\.  Find the area where the child was last seen and clear out the creatures that have been tormenting (?:him|her) in order to bring (?:him|her) out of hiding\.$/,
  :skin                => /^You have been tasked to retrieve (?<number>\d+) (?<skin>[^.]+?)s? of at least (?<quality>[^.]+) quality for (?<npc_name>.+) in (?<town>[^.]+?)\.  You can SKIN them off the corpse of an? (?<creature>[^.]+) or purchase them from another adventurer\.  You can SELL the skins to the furrier as you collect them\."?$/,
  :cull                => Regexp.union(
    /^You have been tasked to(?: help (?<assist>\w+))? suppress (?<creature>[^.]+) activity #{LOCATION_REGEX}\.  You need to kill (?<number>\d+) (?:more\s+)?of them to complete your task\.$/,
    /^You have been tasked to help (?<assist>\w+) rescue a missing child by suppressing (?<creature>[^.]+) activity #{LOCATION_REGEX} during the rescue attempt\.  You need to kill (?<number>\d+) (?:more\s+)?of them to complete your task\.$/,
    /^You have been tasked to help (?<assist>\w+) retrieve an heirloom by suppressing (?<creature>[^.]+) activity #{LOCATION_REGEX} during the retrieval effort\.  You need to kill (?<number>\d+) (?:more\s+)?of them to complete your task\.$/,
    /^You have been tasked to help (?<assist>\w+) kill a dangerous creature by suppressing (?<creature>[^.]+) activity #{LOCATION_REGEX} during the hunt\.  You need to kill (?<number>\d+) (?:more\s+)?of them to complete your task\.$/,
  ),
  :failed              => Regexp.union(
    /^You have failed in your task/,
    /^The child you were tasked to rescue is gone and your task is failed.  Report this failure to the Adventurer's Guild./,
  ),
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(description) ⇒ void

Initializes a new Parser with a bounty task description.

Parameters:

  • description (String)

    the raw bounty task description from the game



142
143
144
# File 'documented/gemstone/bounty/parser.rb', line 142

def initialize(description)
  @description = description
end

Instance Attribute Details

#descriptionObject (readonly)

Returns the value of attribute description.



146
147
148
# File 'documented/gemstone/bounty/parser.rb', line 146

def description
  @description
end

Class Method Details

.parse(desc = checkbounty) ⇒ Hash{Symbol => Object}?

Parses a bounty task description and returns structured task data.

Convenience class method that creates a new Parser instance and calls #parse on it. Returns nil if the description is nil or empty.

Examples:

Parser.parse("You are not currently assigned a task.") #=> {:type=>:none, :requirements=>{}}

Parameters:

  • desc (String, nil) (defaults to: checkbounty)

    the task description; defaults to checkbounty result if not provided

Returns:

  • (Hash{Symbol => Object}, nil)

    parsed task data hash, or nil if desc is empty or no pattern matches



269
270
271
272
273
274
275
# File 'documented/gemstone/bounty/parser.rb', line 269

def self.parse(desc = checkbounty)
  if desc && desc.empty?
    return
  else
    self.new(desc).parse
  end
end

Instance Method Details

#determine_town(captured_town) ⇒ String?

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.

Resolves ambiguous or missing town names based on context clues in the full task description.

Checks for specific guard/guard-like NPC locations and gem dealer references to disambiguate which town is implied, returning the resolved town name or the originally captured town name if no special case matches.

Parameters:

  • captured_town (String, nil)

    the town name from regex captures, if any

Returns:

  • (String, nil)

    the resolved town name, or nil if unresolvable



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'documented/gemstone/bounty/parser.rb', line 242

def determine_town(captured_town)
  if description =~ /the sentry just outside town\.$/
    "Kraken's Fall"
  elsif description =~ /the tavernkeeper at Rawknuckle's Common House\.$/
    "Cold River"
  elsif description =~ /the elderly guard in the East Guardtower\.$/
    "Mist Harbor"
  elsif description =~ /\b(?:Captain|Reiya|Ataum|Galeb)\b/ || description =~ /gem dealer in has received/
    # the latter is a temporary workaround because of an actual typo in the messaging
    # that should be removed if it is ever actually fixed
    'Contempt'
  else
    captured_town
  end
end

#normalized_creature_name(raw_creature_name) ⇒ String

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.

Normalizes certain creature name patterns encountered in task descriptions.

Extracts the significant part of creature names that include modifiers like " being" -> "being" or " magna vereri" -> "magna vereri", and passes through other names unchanged.

Parameters:

  • raw_creature_name (String)

    the creature name from the task description

Returns:

  • (String)

    the normalized creature name



221
222
223
224
225
226
227
228
229
230
# File 'documented/gemstone/bounty/parser.rb', line 221

def normalized_creature_name(raw_creature_name)
  case raw_creature_name
  when /^\w+ being$/
    'being'
  when /^\w+ magna vereri$/
    'magna vereri'
  else
    raw_creature_name
  end
end

#parseHash{Symbol => Object}?

Parses the task description and returns structured task data.

Iterates through TASK_MATCHERS in order to find the first matching regex, then extracts named captures and processes them into a task details hash.

Examples:

description = "You have been tasked to retrieve 5 samples of eonake found in the Dragonspine."
parser = Parser.new(description)
parser.parse #=> {:type=>:herb, :town=>"Ayan", :requirements=>{...}}

Returns:

  • (Hash{Symbol => Object}, nil)

    hash with keys :type and :requirements (plus optional task-specific captures), or nil if no pattern matches

See Also:



160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'documented/gemstone/bounty/parser.rb', line 160

def parse
  TASK_MATCHERS.each do |(task_type, regex)|
    if (md = regex.match(description))
      return (
        {
          type: task_type,
        }.merge(
          task_details_from(md.named_captures)
        ).compact
      )
    end
  end
end

#task_details_from(captures) ⇒ Hash{Symbol => Object}

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.

Converts matched named capture groups into normalized task requirements.

Processes captured values by normalizing town names via #determine_town, converting numeric strings to integers, downcasing action names, normalizing creature names via #normalized_creature_name, and passing through other values unchanged. Always includes :requirements key in returned hash.

Parameters:

Returns:

  • (Hash{Symbol => Object})

    hash with :requirements key and optional :town key, containing processed and normalized captures



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'documented/gemstone/bounty/parser.rb', line 185

def task_details_from(captures)
  {
    requirements: {}
  }.tap do |task_details|
    if (town = determine_town(captures["town"]))
      task_details[:town] = town
      task_details[:requirements][:town] = town
    end

    captures.each do |(key, value)|
      task_details[:requirements][key.to_sym] =
        case key
        when "town"
          town
        when "number"
          value.to_i
        when "action"
          value.downcase
        when "creature"
          normalized_creature_name(value)
        else
          value
        end
    end
  end
end