package TAEB::ScreenScraper;
use TAEB::OO;
use TAEB::Util qw/crow_flies/;
use NetHack::Menu;
use Module::Pluggable (
require => 1,
sub_name => 'load_message_classes',
search_path => ['TAEB::Message'],
);
__PACKAGE__->load_message_classes;
our %msg_string = (
"You are blinded by a blast of light!" =>
['status_change', 'blindness', 1],
"You can see again." =>
['status_change', 'blindness', 0],
"You feel feverish." =>
['status_change', 'lycanthropy', 1],
"You feel purified." =>
['status_change', 'lycanthropy', 0],
"You feel quick!" =>
['status_change' => fast => 1],
"You feel slow!" =>
['status_change' => fast => 0],
"You seem faster." =>
['status_change' => fast => 1],
"You seem slower." =>
['status_change' => fast => 0],
"You feel slower." =>
['status_change' => fast => 0],
"You speed up." =>
['status_change' => fast => 1],
"Your quickness feels more natural." =>
['status_change' => fast => 1],
"You are slowing down." =>
['status_change' => fast => 0],
"Your limbs are getting oozy." =>
['status_change' => fast => 0],
"You slow down." =>
['status_change' => fast => 0],
"Your quickness feels less natural." =>
['status_change' => fast => 0],
"\"and thus I grant thee the gift of Speed!\"" =>
['status_change' => fast => 1],
"You slow down." =>
['status_change' => very_fast => 0],
"Your quickness feels less natural." =>
['status_change' => very_fast => 0],
"You are suddenly moving faster." =>
['status_change' => very_fast => 1],
"You are suddenly moving much faster." =>
['status_change' => very_fast => 1],
"Your knees seem more flexible now." =>
['status_change' => very_fast => 1],
"You feel yourself slowing down." =>
['status_change' => very_fast => 0],
"You feel yourself slowing down a bit." =>
['status_change' => very_fast => 0],
"\"and thus I grant thee the gift of Stealth!\"" =>
['status_change' => stealthy => 1],
# "You feel clumsy." XXX this is also an attribute loss message
"You feel stealthy!" =>
['status_change' => stealthy => 1],
"You feel less stealthy!" =>
['status_change' => stealthy => 0],
"You feel very jumpy." =>
['status_change' => teleporting => 1],
"You feel diffuse." =>
['status_change' => teleporting => 1],
"You feel less jumpy." =>
['status_change' => teleporting => 0],
"From the murky depths, a hand reaches up to bless the sword." =>
['excalibur'],
"The fountain dries up!" =>
['dungeon_feature', 'fountain dries up'],
"As the hand retreats, the fountain disappears!" =>
['dungeon_feature', 'fountain dries up'],
# We need to calculate the amount we would gain _right now_ because
# if we wait, the publisher queue is run after the bottom line and
# we use the new AC. Yuk.
"The air around you begins to shimmer with a golden haze." =>
['protection_add', sub { TAEB->senses->spell_protection_return }],
"The golden haze around you becomes more dense." =>
['protection_add', sub { TAEB->senses->spell_protection_return }],
"The golden haze around you becomes less dense." =>
['protection_dec'],
"The golden haze around you disappears." =>
['protection_gone'],
"This door is locked." =>
['door', 'locked'],
"This door resists!" =>
['door', 'resists'],
"The door resists!" =>
['door', 'resists'],
"WHAMMM!!!" =>
['door', 'resists'],
"You succeed in unlocking the door." =>
['door', 'just_unlocked'],
"You succeed in picking the lock." =>
['door', 'just_unlocked'],
"You stop locking the door." =>
['door', 'interrupted_locking'],
"You stop picking the lock." =>
['door', 'interrupted_unlocking'],
"You stop unlocking the door." =>
['door', 'interrupted_unlocking'],
"You try to move the boulder, but in vain." =>
['immobile_boulder'],
"There is nothing here to pick up." =>
['clear_floor'],
'"You bit it, you bought it!"' =>
['debt' => undef],
"You have no credit or debt in here." =>
['debt', 0],
"You don't owe any money here." =>
['debt', 0],
"There appears to be no shopkeeper here to receive your payment." =>
['debt', 0],
"Your stomach feels content." =>
['nutrition' => 900],
"You hear crashing rock." =>
['pickaxe'],
"Nothing happens." =>
['nothing_happens'],
"A few ice cubes drop from the wand." =>
[wand => 'wand of cold'],
"The wand unsuccessfully fights your attempt to write!" =>
[wand => 'wand of striking'],
"A lit field surrounds you!" =>
[wand => 'wand of light'],
"Far below you, you see coins glistening in the water." =>
[floor_item => sub { TAEB->new_item("1 gold piece") }],
"You wrest one last charge from the worn-out wand." =>
['wrest_wand'],
"You are caught in a bear trap." =>
['beartrap'],
"You can't move your leg!" =>
['beartrap'],
"You are stuck to the web." =>
['web' => 1],
"You can't write on the water!" =>
[dungeon_feature => 'fountain'],
"There is a broken door here." =>
[dungeon_feature => 'brokendoor'],
"The dish washer returns!" =>
['dishwasher'],
"Muddy waste pops up from the drain." =>
['ring_sink'],
"A black ooze gushes up from the drain!" =>
['pudding'],
"Suddenly one of the Vault's guards enters!" =>
['vault_guard' => 1],
"Suddenly, the guard disappears." =>
['vault_guard' => 0],
"\"You've been warned, knave!\"" =>
['vault_guard' => 0],
"You get expelled!" =>
[engulfed => 0],
"You activated a magic portal!" =>
['portal'],
"Something is engraved here on the headstone." =>
['dungeon_feature', 'grave'],
"The heat and smoke are gone." =>
['branch', 'vlad'],
"You smell smoke..." =>
['branch', 'gehennom'],
"A trap door opens up under you!" =>
['trapdoor'],
"There's a gaping hole under you!" =>
['trapdoor'],
"Several flies buzz around the sink." =>
['ring' => 'meat ring'],
"The faucets flash brightly for a moment." =>
['ring' => 'ring of adornment'],
"The sink looks nothing like a fountain." =>
['ring' => 'ring of protection from shape changers'],
"The sink seems to blend into the floor for a moment." =>
['ring' => 'ring of stealth'],
"The water flow seems fixed." =>
['ring' => 'ring of sustain ability'],
"The sink glows white for a moment." =>
['ring' => 'ring of warning'],
"Several flies buzz angrily around the sink." =>
['ring' => 'ring of aggravate monster'],
"The cold water faucet flashes brightly for a moment." =>
['ring' => 'ring of cold resistance'],
"You don't see anything happen to the sink." =>
['ring' => 'ring of invisibility'],
"You see some air in the sink." =>
['ring' => 'ring of see invisible'],
"Static electricity surrounds the sink." =>
['ring' => 'ring of shock resistance'],
"The hot water faucet flashes brightly for a moment." =>
['ring' => 'ring of fire resistance'],
"The sink quivers upward for a moment." =>
['ring' => 'ring of levitation'],
"The sink looks as good as new." =>
['ring' => 'ring of regeneration'],
"The sink momentarily vanishes." =>
['ring' => 'ring of teleportation'],
"You hear loud noises coming from the drain." =>
['ring' => 'ring of conflict'],
"The sink momentarily looks like a fountain." =>
['ring' => 'ring of polymorph'],
"The sink momentarily looks like a regularly erupting geyser." =>
['ring' => 'ring of polymorph control'],
"The sink looks like it is being beamed aboard somewhere." =>
['ring' => 'ring of teleport control'],
"You hear a strange wind." =>
['dungeon_level' => 'oracle'],
"You hear convulsive ravings." =>
['dungeon_level' => 'oracle'],
"You hear snoring snakes." =>
['dungeon_level' => 'oracle'],
"You hear someone say \"No more woodchucks!\"" =>
['dungeon_level' => 'oracle'],
"You hear a loud ZOT!" =>
['dungeon_level' => 'oracle'],
"You enter what seems to be an older, more primitive world." =>
['dungeon_level' => 'rogue'],
"You are being crushed." =>
['grabbed' => 1],
"You get released!" =>
['grabbed' => 0],
"You dig a pit in the floor." =>
['pit' => 1],
"There's not enough room to kick down here." =>
['pit' => 1],
"You can't reach over the edge of the pit." =>
['pit' => 1],
"You float up, out of the pit!" =>
['pit' => 0],
"You crawl to the edge of the pit." =>
['pit' => 0],
"You are still in a pit." =>
['pit' => 1],
"There is a pit here." =>
['pit' => 1],
"You escape a pit." =>
['pit' => 0],
"There's some graffiti on the floor here." =>
['engraving_type' => 'graffiti'],
"You see a message scrawled in blood here." =>
['engraving_type' => 'scrawl'],
"You experience a strange sense of peace." =>
['enter_room','temple'],
"You see no objects here." =>
['clear_floor'],
"You hear the shrill sound of a guard's whistle." =>
['angry_watch'],
"You see an angry guard approaching!" =>
['angry_watch'],
"You're under arrest!" =>
['angry_watch'],
"You are slowing down." =>
['status_change', 'stoning', 1],
"Your limbs are stiffening." =>
['status_change', 'stoning', 1],
"You feel more limber." => # praying
['status_change', 'stoning', 0],
"You feel limber!" => # consuming acid
['status_change', 'stoning', 0],
"You hear somene cursing shoplifters." =>
['level_message', 'shop'],
"You hear the chime of a cash register." =>
['level_message', 'shop'],
"You hear Neiman and Marcus arguing!" => # hallu
['level_message', 'shop'],
"You hear the footsteps of a guard on patrol." =>
['level_message', 'vault'],
"You hear someone counting money." =>
['level_message', 'vault'],
"You hear someone searching." =>
['level_message', 'vault'],
"Your health currently feels amplified!" =>
['resistance_change', 'shock', 1],
"You feel insulated!" =>
['resistance_change', 'shock', 1],
"You feel grounded in reality." =>
['resistance_change', 'shock', 1],
"You strain a muscle." =>
['noise'],
"You kick at empty space." =>
['noise'],
"That hurts!" =>
['noise'],
"This water's no good!" =>
[check => 'inventory'],
"You feel as if you need some help." =>
[check => 'inventory'],
"\"A curse upon thee for sitting upon this most holy throne!\"" =>
[check => 'inventory'],
"Your right leg is in no shape for kicking." =>
[status_change => wounded_legs => 1],
"You hear nothing special." =>
['negative_stethoscope'],
"You hear a voice say, \"It's dead, Jim.\"" =>
['negative_stethoscope'],
"You determine that that unfortunate being is dead." =>
['negative_stethoscope'],
"You couldn't quite make out that last message." =>
['quest_portal'],
"You turn to stone!" =>
['polyself', 'stone golem'],
# there can be other ceiling types
"A trap door in the ceiling opens, but nothing falls out!" =>
[dungeon_feature => trap => 0],
# other ceiling types, other head types
"A trap door in the ceiling opens and a rock falls on your head!" =>
[dungeon_feature => trap => "falling rock trap"],
"You feel a change coming over you." =>
[dungeon_feature => trap => 0],
"Fortunately for you, no boulder was released." =>
[dungeon_feature => trap => 0],
"An arrow shoots out at you!" =>
[dungeon_feature => trap => "arrow trap"],
"A little dart shoots out at you!" =>
[dungeon_feature => trap => "dart trap"],
"You notice a crease in the linoleum." =>
[dungeon_feature => trap => "squeaky board"],
"You notice a loose board below you." =>
[dungeon_feature => trap => "squeaky board"],
"A board beneath you squeaks loudly." =>
[dungeon_feature => trap => "squeaky board"],
"You are enveloped in a cloud of gas!" =>
[dungeon_feature => trap => "sleeping gas trap"],
"A cloud of gas puts you to sleep!" =>
[dungeon_feature => trap => "sleeping gas trap"],
"You land on a set of sharp iron spikes!" =>
[dungeon_feature => trap => "spiked pit"],
"KAABLAMM!!!" =>
[dungeon_feature => trap => "pit"],
# probably issues until we're able to handle traps that relocate you
"A trap door opens up under you!" =>
[dungeon_feature => trap => "trap door"],
"There's a gaping hole under you!" =>
[dungeon_feature => trap => "hole"],
"You take a walk on your web." =>
[dungeon_feature => trap => "web"],
"There is a spider web here." =>
[dungeon_feature => trap => "web"],
# levelport trap message ends with a '.'
"You are momentarily blinded by a flash of light!" =>
[dungeon_feature => trap => "magic trap"],
"You see a flash of light!" =>
[dungeon_feature => trap => "magic trap"],
"You hear a deafening roar!" =>
[dungeon_feature => trap => "magic trap"],
# polymorph
"A shiver runs up and down your spine!" =>
[dungeon_feature => trap => "magic trap"],
"You hear the moon howling at you." =>
[dungeon_feature => trap => "magic trap"],
"You hear distant howling." =>
[dungeon_feature => trap => "magic trap"],
"Your pack shakes violently!" =>
[dungeon_feature => trap => "magic trap"],
"You smell hamburgers." =>
[dungeon_feature => trap => "magic trap"],
"You smell charred flesh." =>
[dungeon_feature => trap => "magic trap"],
# can also get this when losing sleep res
#"You feel tired."
"You feel momentarily lethargic." =>
[dungeon_feature => trap => "anti-magic trap"],
"You feel momentarily different." =>
[dungeon_feature => trap => "polymorph trap"],
"Click! You trigger a rolling boulder trap!" =>
[dungeon_feature => trap => "rolling boulder trap"],
"You activated a magic portal!" =>
[dungeon_feature => trap => "magic portal"],
'You are suddenly in familiar surroundings.' =>
[quest_entrance => 'Arc'],
'Warily you scan your surroundings,' =>
[quest_entrance => 'Bar'],
'You descend through a barely familiar stairwell' =>
[quest_entrance => 'Cav'],
'What sorcery has brought you back to the Temple' =>
[quest_entrance => 'Hea'],
'You materialize in the shadows of Camelot Castle.' =>
[quest_entrance => 'Kni'],
'You find yourself standing in sight of the Monastery' =>
[quest_entrance => 'Mon'],
'You find yourself standing in sight of the Great' =>
[quest_entrance => 'Pri'],
'You arrive in familiar surroundings.' =>
[quest_entrance => 'Ran'],
'Even before your senses adjust, you recognize the kami' =>
[quest_entrance => 'Sam'],
'You breathe a sigh of relief as you find yourself' =>
[quest_entrance => 'Tou'],
'You materialize at the base of a snowy hill.' =>
[quest_entrance => 'Val'],
'You are suddenly in familiar surroundings.' =>
[quest_entrance => 'Wiz'],
'They are cursed.' =>
['cursed'],
'It is cursed.' =>
['cursed'],
'You start to float in the air!' =>
[status_change => levitation => 1],
'You float gently to the floor.' =>
[status_change => levitation => 0],
'You are floating high above the stairs.' =>
[status_change => levitation => 1],
'You have nothing to brace yourself against.' =>
[status_change => levitation => 1],
'You cannot reach the ground.' =>
[status_change => levitation => 1],
'You are floating high above the fountain.' =>
[status_change => levitation => 1],
'Your sacrifice sprouts wings and a propeller and roars away!' =>
['sacrifice_gone'],
'Your sacrifice puffs up, swelling bigger and bigger, and pops!' =>
['sacrifice_gone'],
'Your sacrifice collapses into a cloud of dancing particles and fades away!' =>
['sacrifice_gone'],
'Your sacrifice disappears!' =>
['sacrifice_gone'],
'Your sacrifice disappears in a flash of light!' =>
['sacrifice_gone'],
'Your sacrifice disappears in a burst of flame!' =>
['sacrifice_gone'],
'The blood covers the altar!' =>
['sacrifice_gone'],
'You fall through...' =>
['trapdoor'],
'You have no secondary weapon readied.' =>
['slot_empty', 'offhand'],
);
our @msg_regex = (
[
qr/^You are(?: already)? empty .*\.$/,
['slot_empty', 'weapon'],
],
[
qr/^You (?:turn into an?|feel like a new)(?: female| male|) ([^!]*)!$/,
# Luckily, all the base races are M2_NOPOLY.
['polyself', sub {
$1 =~ /man|woman|elf|dwarf|gnome|orc/ ? undef : $1; }],
],
[
qr/^You offer the Amulet of Yendor to .*$/,
['sacrifice_gone'],
],
[
qr/^The blood floods the altar, which vanishes in a .* cloud!$/,
['sacrifice_gone'],
],
[
qr/^You return to .* form!$/,
['polyself', undef],
],
[
qr/^The altar is stained with .* blood.$/,
['sacrifice_gone'],
],
[
qr/^The .* appears to be in ex(?:cellent|traordinary) health for a statue.$/,
['negative_stethoscope'],
],
[
qr/^Your legs? feels? somewhat better\.$/,
[status_change => wounded_legs => 0],
],
[
qr/^You can't go (?:up|down) here\.$/,
['dungeon_feature', 'bad staircase'],
],
[
# NetHack will not send "There are no items here." if there is a
# terrain feature at the current location. To work around this, we
# need to clear the floor on receiving notices of terrain... HOWEVER
# if there were a lot of items, we handle menus before messages. To
# avoid a big mess, we skip the clear in that case.
qr/^There is (?:molten lava|ice|an? .*) here.$/,
[sub { TAEB->scraper->saw_floor_list_this_step ?
'' : 'clear_floor' }], # is this the best way?
],
[
qr/^There is a (staircase (?:up|down)|fountain|sink|grave) here\.$/,
['dungeon_feature', sub { $1 }],
],
[
qr/^You feel more confident in your (?:(weapon|spell casting|fighting) )?skills\.$/,
[check => 'enhance'],
],
# There's no message for cursing intervene() while blind and MRless :(
[
qr/^You notice a .* glow surrounding you\.$/, # sic: "a orange glow"
[check => 'inventory'],
],
# this can be the only message we get, if blind and MRless
[
qr/^The voice of.*: "Thou hast angered me\."$/,
[check => 'inventory'],
],
[
qr/^You cannot escape from (?:the )?(.*)!/,
['cannot_escape', sub { $1 || '' }],
],
[
qr/^You throw (\d+) /,
['throw_count', sub { $1 }],
],
[
qr/^(?:A|Your) bear trap closes on your/,
['beartrap'],
],
[
qr/^You fall into (?:a|your) pit!/,
['pit' => 1]
],
[
qr/^You stumble into (?:a|your) spider web!/,
['web' => 1]
],
[
qr/^You (?:see|feel) here (.*?)\./,
['floor_item', sub {
TAEB->enqueue_message('clear_floor');
TAEB->new_item($1); }],
],
[
qr/^You feel no objects here\./,
['clear_floor']
],
[
qr/^(?:You have a little trouble lifting )?(. - .*?|\d+ gold pieces?)\.$/,
['got_item', sub { TAEB->new_item($1) }],
],
[
qr/^You read: \"(.*)\"\./,
['floor_message', sub { (my $str = $1) =~ tr/_/ /; $str }],
],
[
qr/^You owe .*? (\d+) zorkmids?\./,
['debt', sub { $1 }],
],
[
qr/^You do not owe .* anything\./,
['debt' => 0],
],
[
qr/^The engraving on the .*? vanishes!/,
[wand => map { "wand of $_" } 'teleportation', 'cancellation', 'make invisible'],
],
[
qr/^The bugs on the .*? stop moving!/,
[wand => 'wand of death', 'wand of sleep'],
],
[
# digging, fire, lightning
qr/^This .*? is a (wand of \S+)!/,
[wand => sub { $1 }],
],
[
qr/^The .*? is riddled by bullet holes!/,
[wand => 'wand of magic missile'],
],
[
# slow monster, speed monster
qr/^The bugs on the .*? (slow|speed) (?:up|down)\!/,
[wand => sub { "wand of $1 monster" }],
],
[
qr/^The engraving now reads:/,
[wand => 'wand of polymorph'],
],
[
qr/^You (add to the writing|write) in the dust with a.* wand of (create monster|secret door detection)/,
[wand => sub { "wand of $2" }],
],
[
qr/^.*? zaps (?:(?:him|her|it)self with )?an? .*? wand!/,
['check' => 'discoveries'],
],
[
qr/^"Usage fee, (\d+) zorkmids?\."/,
[debt => sub { $1 }],
],
[
qr/ \(unpaid, \d+ zorkmids?\)/,
[debt => undef],
],
[
qr/^There are (?:several|many) (?:more )?objects here\./,
[check => 'floor'],
],
[
qr/^(?:(?:The .*?)|She|It) (?:steals|stole) (.*)(?:\.|\!)/,
[lost_item => sub { TAEB->new_item($1) }],
],
[
qr/^You are (?:almost )?hit by /,
[check => 'floor'],
],
[
qr/"Please (?:drop that gold and )?follow me."/ =>
['vault_guard' => 1],
],
[
qr/"I repeat, (?:drop that gold and )?follow me!"/ =>
['vault_guard' => 1],
],
[
qr/^(.*?) engulfs you!/ =>
['engulfed' => 1],
],
[
qr/^(.*?) reads a scroll / =>
[check => 'discoveries'],
],
[
qr/^(.*?) drinks an? .* potion|^(.*?) drinks a potion called / =>
[check => 'discoveries'],
],
[
qr/^Autopickup: (ON|OFF)/ =>
['autopickup' => sub { $1 eq 'ON' }],
],
[
qr/^You (?:kill|destroy) (?:the|an?)(?: invisible)? (.*)(?:\.|!)/ =>
['killed' => sub { $1 } ],
],
[
qr/^Suddenly, .* vanishes from the sink!/ =>
['ring' => 'ring of hunger'],
],
[
qr/^The sink glows (silver|black) for a moment\./ =>
['ring' => 'ring of protection'],
],
[
qr/^The water flow seems (greater|lesser) now.\./ =>
['ring' => 'ring of gain constitution'],
],
[
qr/^The water flow seems (stronger|weaker) now.\./ =>
['ring' => 'ring of gain strength'],
],
[
qr/^The water flow (hits|misses) the drain\./ =>
['ring' => 'ring of increase accuracy'],
],
[
qr/^The water's force seems (greater|smaller) now\./ =>
['ring' => 'ring of increase damage'],
],
[
qr/^You smell rotten (.*)\./ =>
['ring' => 'ring of poison resistance'],
],
[
qr/^You thought your (.*) got lost in the sink, but there it is!/ =>
['ring' => 'ring of searching'],
],
[
qr/^You see (.*) slide right down the drain!/ =>
['ring' => 'ring of free action'],
],
[
qr/(.*) is regurgitated!/ =>
['ring' => 'ring of slow digestion'],
],
[
qr/^You stop eating the (.*)\./ =>
['stopped_eating' => sub { $1 } ],
],
[
qr/You add the .* spell to your repertoire/ =>
[check => 'spells'],
],
[
qr/You add the (.*) spell to your repertoire/ =>
['check' => 'discoveries'],
],
[
qr/You add the (.*) spell to your repertoire/ =>
['learned_spell' => sub { $1 }],
],
[
qr/crashes on .* and breaks into shards/ =>
['check' => 'discoveries'],
],
[ # Avoid matching shopkeeper name by checking for capital lettering.
qr/Welcome(?: again)? to(?> [A-Z]\S+)+ ([a-z ]+)!/ =>
['enter_room',
sub {
(
$1 eq 'treasure zoo' ? 'zoo' : 'shop',
TAEB::Spoilers::Room->shop_type($1)
)
},
],
],
[
qr/You have a(?: strange) forbidding feeling\./ =>
['enter_room','temple'],
],
[
qr/.* (?:grabs|swings itself around) you!/ =>
['grabbed' => 1],
],
[
qr/You cannot escape from .*!/ =>
['grabbed' => 1],
],
[
qr/.* (?:releases you!|grip relaxes\.)/ =>
['grabbed' => 0],
],
[
qr/^Some text has been (burned|melted) into the (?:.*) here\./ =>
['engraving_type' => sub { $1 } ],
],
[
qr/^Something is (written|engraved) here (?:in|on) the (?:.*)\./ =>
['engraving_type' => sub { $1 } ],
],
[
qr/^(?:(?:The )?(.*|Your)) medallion begins to glow!/ =>
['life_saving' => sub { $1 } ],
],
[
qr/^There is an altar to [\w\- ]+ \((law|neu|cha|unaligned)\w*\) here\./ =>
['dungeon_feature' => sub { ucfirst($1) .' altar' } ],
],
[
qr/^There's a (.*?) hiding under a (.*)!/ =>
['hidden_monster' => sub { ($1, $2) } ],
],
[
qr/^What a pity - you just ruined a future piece of (?:fine )?art!/ =>
['status_change', 'stoning', 0],
],
[
qr/^(.*?) (hits|misses)[.!]$/ =>
['attacked' => sub { $1, $2 eq 'hits' } ],
],
[
qr/^Your .* get new energy\.$/ =>
[status_change => very_fast => 1],
],
[
# This one is somewhat tricky. There is no message for speed ending
# if you are still very fast due to speed boots, so speed will stay
# at 'fast'. This causes no harm until the boots are taken off or
# destroyed; fortunately at that time we receive the following message,
# which allows us to fix the mistaken speed.
qr/^You feel yourself slow down.*\.$/ =>
[status_change => very_fast => 0],
],
[
qr/^You (?:be chillin'|feel a momentary chill)\.$/ =>
['resistance_change', 'fire', 1],
],
[
qr/^You feel (?:warm\!|full of hot air\.)$/ =>
['resistance_change', 'cold', 1],
],
[
qr/^You feel (?:very firm|totally together, man)\.$/ =>
['resistance_change', 'disintegration', 1],
],
[
qr/^You feel(?: especially)? (?:healthy|hardy)(?:\.|\!)$/ =>
['resistance_change', 'poison', 1],
],
[
qr/^You feel(?: wide)? awake(?:\.|\!)$/ =>
['resistance_change', 'sleep', 1],
],
[
qr/^You're finally finished\./ =>
['finally_finished'],
],
[
qr/Air currents pull you down into \w+ (hole|pit)!/ =>
[dungeon_feature => trap => sub { $1 }],
],
[
qr/You (?:float|fly) over \w+ (.*)\./ =>
[dungeon_feature => trap => sub { $1 }],
],
[
qr/You escape \w+ (.*)\./ =>
[dungeon_feature => trap => sub { $1 }],
],
[
qr/You hear a (?:loud|soft) click(?:!|\.)/ =>
[dungeon_feature => trap => 0],
],
[
qr/You (?:burn|dissolve) \w+ spider web!/ =>
[dungeon_feature => trap => 0],
],
[
qr/You tear through \w+ web!/ =>
[dungeon_feature => trap => 0],
],
[
qr/\w+ bear trap closes harmlessly (?:through|over) you\./ =>
[dungeon_feature => trap => "bear trap"],
],
[
# polymorph issues
qr/A gush of water hits you(?: on the head|r (?:left|right) arm)!/ =>
[dungeon_feature => trap => "rust trap"],
],
[
qr/You see \w+ ((?:spiked )?pit) below you\./ =>
[dungeon_feature => trap => sub { $1 }],
],
[
qr/\w+ pit (full of spikes )?opens up under you!/ =>
[dungeon_feature => trap => sub { $1 ? "spiked pit" : "pit" }],
],
[
# steed issues
qr/You fall into \w+ pit!/ =>
[dungeon_feature => trap => "pit"],
],
[
qr/You flow through \w+ spider web\./ =>
[dungeon_feature => trap => "web"],
],
[
qr/You stumble into \w+ spider web!/ =>
[dungeon_feature => trap => "web"],
],
[
qr/You feel (?:oddly )?like the prodigal son\./ =>
[dungeon_feature => trap => "magic trap"],
],
[
qr/You suddenly yearn for (?:Cleveland|your (?:nearby|distant) homeland)\./ =>
[dungeon_feature => trap => "magic trap"],
],
[
qr/(?:There is|You discover) (?:the )?trigger(?: of your mine)? in a pile of soil below you\./ =>
[dungeon_feature => trap => "land mine"],
],
[
qr/(.*) slips? as you throw it!/ =>
[throw_slip => sub { $1 }],
],
[
qr/^Your (.*?) corpses? rots? away\./ =>
[corpse_rot => sub { $1 }],
],
);
our @god_anger = (
qr/^You feel that .*? is (bummed|displeased)\.$/ => 1,
qr/^"Thou must relearn thy lessons!"$/ => 3,
qr/^"Thou durst (scorn|call upon) me\?"$/ => 8,
qr/^Suddenly, a bolt of lightning strikes you!$/ => 10000,
qr/^Suddenly a bolt of lightning comes down at you from the heavens!$/ => 10000,
);
for (my $i = 0; $i < @god_anger; $i += 2) {
push @msg_regex, [
$god_anger[$i],
['god_angry' => $god_anger[$i+1]],
];
}
our @prompts = (
qr/^What do you want to write with\?/ => 'write_with',
qr/^What do you want to dip\?/ => 'dip_what',
qr/^What do you want to dip into\?/ => 'dip_into_what',
qr/^What do you want to throw\?/ => 'throw_what',
qr/^What do you want to wield\?/ => 'wield_what',
qr/^What do you want to use or apply\?/ => 'apply_what',
qr/^In what direction\?/ => 'what_direction',
qr/^In what direction do you want .*\?/ => 'what_direction',
qr/^Talk to whom\? \(in what direction\)/ => 'what_direction',
qr/^Itemized billing\? \[yn\] \(n\)/ => 'itemized_billing',
qr/^Lock it\?/ => 'lock',
qr/^Unlock it\?/ => 'unlock',
qr/^Drink from the (fountain|sink)\?/ => 'drink_from',
qr/^What do you want to drink\?/ => 'drink_what',
qr/^What do you want to eat\?/ => 'eat_what',
qr/^What do you want to sacrifice\?/ => 'sacrifice_what',
qr/^What do you want to zap\?/ => 'zap_what',
qr/^What do you want to read\?/ => 'read_what',
qr/^What do you want to rub\?/ => 'rub_what',
qr/^What do you want to rub on .*?\?/ => 'rub_on_what',
qr/^For what do you wish\?/ => 'wish',
qr/^Really attack (.*?)\?/ => 'really_attack',
qr/^This spellbook is difficult to comprehend/ => 'difficult_spell',
qr/^Dip (.*?) into the (fountain|pool of water|water|moat)\?/ => 'dip_into_water',
qr/^There (?:is|are) (.*?) here; eat (?:it|one)\?/ => 'eat_ground',
qr/^There (?:is|are) (.*?) here; sacrifice (?:it|one)\?/ => 'sacrifice_ground',
qr/^What do you want to (?:write|engrave|burn|scribble|scrawl|melt) (?:in|into|on) the (.*?) here\?/ => 'write_what',
qr/^What do you want to add to the (?:writing|engraving|grafitti|scrawl|text) (?:in|on|melted into) the (.*?) here\?/ => 'write_what',
qr/^Do you want to add to the current engraving\?/ => 'add_engraving',
qr/^Name an individual object\?/ => 'name_specific',
qr/^What do you want to (?:call|name)\?/ => 'name_what',
qr/^Call (.*?):/ => 'name',
qr/^What do you want to wear\?/ => 'wear_what',
qr/^What do you want to put on\?/ => 'wear_what',
qr/^What do you want to remove\?/ => 'remove_what',
qr/^What do you want to take off\?/ => 'remove_what',
qr/^Which ring-finger, Right or Left\?/ => 'which_finger',
qr/^(.*?) for (\d+) zorkmids?\. Pay\?/ => 'buy_item',
qr/You did (\d+) zorkmids worth of damage!/ => 'buy_door',
qr/^"Hello stranger, who are you\?"/ => 'vault_guard',
qr/^How much will you offer\?/ => 'donate',
qr/^Do you want to keep the save file\?/ => 'save_file',
qr/^Advance skills without practice\?/ => 'advance_without_practice',
qr/^Stop eating\?/ => 'stop_eating',
qr/^You have (?:a little|much) trouble lifting .*\. Continue\?/ => 'continue_lifting',
qr/^Beware, there will be no return! Still climb\?/ => 'really_escape',
);
our @exceptions = (
qr/^You don't have that object/ => 'missing_item',
qr/^You don't have anything to (?:zap|eat)/ => 'missing_item',
qr/^You don't have anything else to wear/ => 'missing_item',
qr/^You are too hungry to cast that spell/ => 'hunger_cast',
);
our @location_requests = (
qr/^To what position do you want to be teleported\?/ => 'controlled_tele',
);
has messages => (
is => 'rw',
isa => 'Str',
);
has old_messages => (
is => 'ro',
isa => 'ArrayRef',
auto_deref => 1,
default => sub { [] },
);
has parsed_messages => (
is => 'ro',
isa => 'ArrayRef',
auto_deref => 1,
default => sub { [] },
);
has calls_this_turn => (
metaclass => 'Counter',
is => 'ro',
provides => {
inc => 'inc_calls_this_turn',
reset => 'reset_calls_this_turn',
},
);
has saw_floor_list_this_step => (
is => 'rw',
isa => 'Bool',
default => 0,
);
sub _recurse {
local $SIG{__DIE__};
die "Recursing screenscraper.\n";
}
sub scrape {
my $self = shift;
$self->check_cycling;
eval {
# You don't have that object!
$self->handle_exceptions;
# handle ^X
$self->handle_attributes;
# handle --More-- menus
$self->handle_more_menus;
# handle death messages
$self->handle_game_end;
# handle menus
$self->handle_menus;
# handle --More--
$self->handle_more;
# handle other text
$self->handle_fallback;
# handle location requests
$self->handle_location_request;
# publish messages for all_messages
$self->send_messages;
};
if (($@ || '') =~ /^Recursing screenscraper/) {
@_ = 'TAEB';
goto TAEB->can('process_input');
}
elsif ($@) {
local $SIG{__DIE__}; # don't need to log again
die "$@\n";
}
}
sub check_cycling {
my $self = shift;
$self->inc_calls_this_turn;
if ($self->calls_this_turn > 500) {
TAEB->log->scraper("It seems I'm iterating endlessly and making no progress. I'm going to attempt to save and exit!", level => 'critical');
TAEB->save;
}
}
sub msg_turn {
my $self = shift;
$self->reset_calls_this_turn;
}
sub msg_step {
my $self = shift;
$self->saw_floor_list_this_step(0);
}
sub clear {
my $self = shift;
$self->messages('');
splice @{ $self->parsed_messages };
}
sub handle_exceptions {
my $response = TAEB->get_exceptional_response(TAEB->topline);
if (defined $response) {
TAEB->write($response);
_recurse;
}
}
sub handle_more {
my $self = shift;
# while there's a --More-- on the screen..
while (TAEB->vt->as_string =~ /^(.*?)--More--/) {
# add the text to the buffer
$self->messages($self->messages . ' ' . $1);
# try to get rid of the --More--
TAEB->write(' ');
_recurse;
}
}
sub handle_attributes {
my $self = shift;
my ($method, $attribute);
if (TAEB->topline =~ /^(\s+)Base Attributes/) {
my $start = length($1);
my $skip = $start + 17;
(my $name = substr(TAEB->vt->row_plaintext(3), $skip)) =~ s/ //g;
TAEB->name($name);
# Alignment may end up on line 13 or 14 depending on if we are
# polymorphed into something with a different gender
# 4: race 5: role 12: gender 13-14: align
for (4, 5, 12, 13, 14) {
next unless my ($method, $attribute) =
substr(TAEB->vt->row_plaintext($_), $start) =~
m/(race|role|gender|align)(?:ment)?\s+: (.*)\b/;
$attribute = substr($attribute, 0, 3);
$attribute = ucfirst lc $attribute;
TAEB->$method($attribute);
}
# can't go in the loop above because it collides with race
my ($polyrace) = substr(TAEB->vt->row_plaintext(10), $start) =~
m/race\s+: (.*?)\s*$/;
TAEB->polyself($polyrace =~ /^(?:orc|elf|gnome|dwarf|human)$/ ?
undef : $polyrace);
TAEB->log->scraper(sprintf 'It seems we are a %s %s %s %s named %s.', TAEB->role, TAEB->race, TAEB->gender, TAEB->align, TAEB->name);
TAEB->enqueue_message('character', TAEB->name, TAEB->role, TAEB->race,
TAEB->gender, TAEB->align);
TAEB->write(' ');
_recurse;
}
# wizmode data; we should parse this eventually
elsif (TAEB->topline =~ /^\s*Current Attributes:\s*$/) {
TAEB->write(' ');
_recurse;
}
}
sub handle_more_menus {
my $self = shift;
my $each;
my $line_3 = 0;
if (TAEB->topline =~ /^\s*Discoveries\s*$/) {
$each = sub {
my ($identity, $appearance) = /^\s+(.*?) \((.*?)\)/
or return;
TAEB->log->scraper("Discovery: $appearance is $identity");
TAEB->enqueue_message('discovery', $identity, $appearance);
};
}
elsif (TAEB->topline =~ /Things that (?:are|you feel) here:/
|| ($line_3 = TAEB->vt->row_plaintext(2) =~ /Things that (?:are|you feel) here:/)
) {
$self->messages($self->messages . ' ' . TAEB->topline) if $line_3;
TAEB->enqueue_message('clear_floor');
$self->saw_floor_list_this_step(1);
my $skip = 1;
$each = sub {
# skip the items until we get "Things that are here" which
# typically is a message like "There is a door here"
do { $skip = 0; return } if /^\s*Things that are here:/;
return if $skip;
my $item = TAEB->new_item($_);
TAEB->log->scraper("Adding $item to the current tile.");
TAEB->enqueue_message('floor_item' => $item);
return 0;
};
}
elsif (TAEB->state eq 'dying' && TAEB->topline =~ /Voluntary challenges:\s*$/) {
my $skip = 2;
$each = sub {
return if $skip-- > 0;
s/\s+$//;
s{^You were vegetarian\.$} {vegetarian} ||
s{^You followed a strict vegan diet\.$} {vegan} ||
s{^You went without food\.$} {foodless} ||
s{^You were an atheist\.$} {atheist} ||
s{^You never hit with a wielded weapon\.$}{weaponless} ||
s{^You were illiterate\.$} {illiterate} ||
s{^You never genocided any monsters\.$} {genoless} ||
s{^You never polymorphed an object\.$} {polyitemless} ||
s{^You never changed form\.$} {polyselfless} ||
s{^You used no wishes\.$} {wishless} ||
s{^You did not wish for any artifacts\.} {artiwishless} ||
s{^You were a pacifist\.} {pacifist} ||
(/^You used \d+ wish(es)\./ && next) ||
(/You used a wielded weapon \d+ times?\./ && next) ||
TAEB->log->scraper("Unable to parse conduct string '$_'.");
TAEB->death_report->add_conduct($_);
};
}
if ($each) {
my $iter = 0;
while (1) {
++$iter;
# find the first column the menu begins
my ($endrow, $begincol);
my $lastrow_contents = TAEB->vt->row_plaintext(TAEB->vt->y);
if ($lastrow_contents =~ /^(.*?)--More--/) {
$endrow = TAEB->vt->y;
$begincol = length $1;
}
else {
_recurse if $iter > 1;
die "Unable to find --More-- on the end row: $lastrow_contents";
}
if ($iter > 1) {
# on subsequent iterations, the --More-- will be in the second
# column when the menu is continuing
_recurse if $begincol != 1;
}
# now for each menu line, invoke the coderef
for my $row (0 .. $endrow - 1) {
local $_ = TAEB->vt->row_plaintext($row, $begincol, 80);
$each->($_);
}
# get to the next page of the menu
TAEB->write(' ');
TAEB->process_input(0);
}
}
}
sub handle_menus {
my $self = shift;
my $menu = NetHack::Menu->new(vt => TAEB->vt);
my $selector;
my $committer = sub { $menu->commit };
if (TAEB->topline =~ /Pick up what\?/) {
$selector = TAEB->menu_select('pickup');
}
elsif (TAEB->topline =~ /Pick a skill to advance/) {
$selector = TAEB->menu_select('enhance');
}
elsif (TAEB->topline =~ /What would you like to identify first\?/) {
$selector = TAEB->menu_select('identify');
}
elsif (TAEB->topline =~ /Choose which spell to cast/) {
my $which_spell = TAEB->is_checking('spells') ? "\e"
: TAEB->single_select('cast') || "\e";
$committer = sub { $which_spell };
$selector = sub {
my $slot = shift;
# force bolt 1 attack 0%
my ($name, $forgotten, $fail) =
/^(.*?)\s+\d([ *])\s+\w+\s+(\d+)%\s*$/
or return;
TAEB->enqueue_message('know_spell',
$slot, $name, $forgotten eq '*', $fail);
return;
};
}
elsif (TAEB->topline =~ /What would you like to drop\?/) {
# this one is special: it'll handle updating the inventory
my %dont_have = map { $_, 1 } 'a' .. 'z', 'A' .. 'Z';
$selector = sub {
my $slot = shift;
my $new_item = TAEB->new_item($_);
TAEB->inventory->update($slot => $new_item);
my $item = TAEB->inventory->get($slot);
delete $dont_have{$slot} if $item;
# if we can drop the item, drop it!
if (!TAEB->is_checking('inventory')) {
# $item will be undef for non-inventory items like gold
my $drop = TAEB->ai->drop($item || $new_item);
# dropping a part of the stack
if (ref($drop) && $$drop < $item->quantity) {
my $new_item = $item->fork_quantity($$drop);
TAEB->enqueue_message('floor_item' => $new_item);
return $$drop;
}
# dropping the whole stack
elsif ($drop) {
TAEB->inventory->remove($slot) if $item;
TAEB->enqueue_message('floor_item' => $item);
return 'all';
}
}
return 0;
};
$committer = sub {
for my $slot (keys %dont_have) {
my $item = TAEB->inventory->get($slot);
if ($item) {
TAEB->log->scraper("$item seems to have disappeared!",
level => 'warning');
TAEB->inventory->remove($slot);
}
}
$menu->commit;
};
}
return unless $menu->has_menu;
until ($menu->at_end) {
TAEB->write($menu->next);
TAEB->process_input(0);
}
$menu->select_quantity($selector) if $selector;
TAEB->write($committer->());
_recurse;
}
sub handle_fallback {
my $self = shift;
my $topline = TAEB->topline;
$topline =~ s/\s+$/ /;
my $response_needed = TAEB->vt->y == 0
|| (TAEB->vt->y == 1 && TAEB->vt->x == 0);
# Prompt that spills onto the next line
if (!$response_needed && TAEB->vt->y == 1) {
my $row_one = TAEB->vt->row_plaintext(1);
$row_one =~ s/\s+$//;
# NetHack clears the rest of the line when it continues the prompt
# to the next line. We need to be strict here to avoid false positives
if ($row_one =~ /^\S/ && length($row_one) == TAEB->vt->x - 1) {
TAEB->log->scraper("Appending '$row_one' to the topline since it appears to be a continuation.");
$topline .= $row_one;
$response_needed = 1;
}
}
$self->messages($self->messages . ' ' . $topline);
if ($response_needed) {
my $response = TAEB->get_response($topline);
if (defined $response) {
$self->messages($self->messages . $response);
TAEB->write($response);
_recurse;
}
else {
$self->messages($self->messages . "(escaped)");
TAEB->write("\e");
TAEB->log->scraper("Escaped out of unhandled prompt: " . $topline, level => 'warning');
_recurse;
}
}
}
sub handle_location_request {
my $self = shift;
return unless $self->messages =~
/(?:^\s*| )(.*?) \(For instructions type a \?\)\s*$/;
my $type = $1;
my $dest = TAEB->get_location_request($type);
if (defined $dest) {
$self->messages($self->messages . sprintf "(%d, %d)",
$dest->x, $dest->y);
TAEB->write(crow_flies(TAEB->vt->x, TAEB->vt->y,
$dest->x, $dest->y) . ".");
_recurse;
}
else {
$self->messages($self->messages . "(escaped)");
TAEB->write("\e");
TAEB->log->scraper("Escaped out of unhandled location request: " . $type, level => 'warning');
_recurse;
}
}
sub handle_game_end {
my $self = shift;
if (TAEB->topline =~ /^Do you want your possessions identified\?|^Die\?|^Really quit\?|^Do you want to see what you had when you died\?/) {
TAEB->state('dying');
TAEB->write('y');
TAEB->log->scraper("Oh no! We died!");
TAEB->death_state('inventory');
_recurse;
}
if (TAEB->topline =~ /^Really save\?/) {
TAEB->write('y');
die "The game has been saved.\n";
}
return unless TAEB->state eq 'dying';
if (TAEB->topline =~ /^Save bones\?|^Dump core\?/) {
TAEB->write('n');
_recurse;
}
if (TAEB->topline =~ /Final Attributes:\s*$/) {
TAEB->death_state('attributes');
# XXX: parse attributes
TAEB->write(' ');
_recurse;
}
elsif (TAEB->topline =~ /Vanquished creatures:\s*$/) {
TAEB->death_state('kills');
# XXX: parse kills
TAEB->write(' ');
_recurse;
}
elsif (TAEB->topline =~ /Voluntary challenges:\s*$/) {
TAEB->death_state('conducts');
# We parse conducts in handle_more_menus
TAEB->write(' ');
_recurse;
}
elsif (TAEB->topline =~ /^(Fare thee well|Sayonara|Aloha|Farvel|Goodbye) /) {
TAEB->death_state('summary');
TAEB->death_report->score($1)
if TAEB->vt->row_plaintext(2) =~ /(\d+) points?/;
TAEB->death_report->turns($1)
if TAEB->vt->row_plaintext(3) =~ /(\d+) moves?/;
# summary is always one page, so after that is high scores with no
# "press space to close nethack"
TAEB->write(' ');
# at this point the nethack process has now ended
die "The game has ended.\n";
}
# No easy thing to check for here, so assume death_state isn't lying to us
elsif (TAEB->death_state eq 'inventory') {
TAEB->write(' ');
# XXX: parse inventory
_recurse;
}
# No easy thing to check for on subsequent pages, so again assume
# death_state is honest
elsif (TAEB->death_state eq 'kills') {
TAEB->write(' ');
# XXX: parse kills
_recurse;
}
die "We're dying but I don't understand the message " . TAEB->topline;
}
sub all_messages {
my $self = shift;
local $_ = $self->messages;
# XXX: hack here: replacing all spaces in an engraving with underscores
# so that our message parsing (which just splits on double spaces)
# doesn't explode
s{You read: +"(.*)"\.}{
(my $copy = $1) =~ tr/ /_/;
q{You read: "} . $copy . q{".}
}e;
s/\s+ / /g;
my @messages = grep { length }
map { (my $trim = $_) =~ s/^\s+//; $trim =~ s/\s+$//; $trim }
split / /, $_;
return join $_[0], @messages
if @_;
return @messages;
}
=head2 send_messages
Iterate over all_messages, invoking TAEB->enqueue_message for each one we know
about.
=cut
sub send_messages {
my $self = shift;
for my $line ($self->all_messages) {
study $line;
my @messages;
if (exists $msg_string{$line}) {
push @messages, [
map { ref($_) eq 'CODE' ? $_->() : $_ }
@{ $msg_string{$line} }
];
}
for my $something (@msg_regex) {
if ($line =~ $something->[0]) {
push @messages, [
map { ref($_) eq 'CODE' ? $_->() : $_ }
@{ $something->[1] }
];
}
}
if (@messages) {
my $msg_names = join ', ', map { $_->[0] } @messages;
TAEB->log->scraper("Sending '$msg_names' in response to '$line'");
TAEB->enqueue_message(@$_) for @messages;
}
else {
TAEB->log->scraper("I don't understand this message: $line");
}
push @{ $self->parsed_messages }, [$line => scalar @messages];
push @{ $self->old_messages }, $line;
shift @{ $self->old_messages } if @{ $self->old_messages } > 100;
}
}
=head2 farlook Int, Int -> (Str | Str, Str, Str, Str)
This will farlook (the C<;> command) at the given coordinates and return
whatever's there.
In scalar context, it will return the plain description string given by
NetHack. In list context, it will return the components: glyph, genus, species,
and how the monster is visible (infravision, telepathy, etc).
WARNING: Since this method interacts with NetHack directly, you cannot use it
in callbacks where there is menu interaction or (in general) any place except
command mode.
=cut
sub farlook {
my $self = shift;
my $tile = shift;
my $directions = crow_flies($tile->x, $tile->y);
# We compare the messages before farlooking to the messages after
# farlooking, to get just the messages corresponding to the
# farlook itself.
my $messagesbefore = TAEB->messages;
TAEB->write(';' . $directions . '.');
TAEB->process_input;
# use TAEB->messages as it may consist of multiple lines
my $description = TAEB->messages;
if ($description ne $messagesbefore) {
$description =~ s/^\Q$messagesbefore\E\s*//;
}
return $description =~ /^(.)\s*(.*?)\s*\((.*)\)\s*(?:\[(.*)\])?\s*$/
if wantarray;
return $description;
}
__PACKAGE__->meta->make_immutable;
no TAEB::OO;
1;