Automatic price-identification of scrolls in Nethack

23

3

OH GODS NO!! You can't leave us here with Doorknob! It'll be nethack everywhere! - 1d ago by Geobits

Well, couldn't disappoint...

Introduction

(you can skip this section if you don't care about exposition and/or if you have Tab Explosion Syndrome)

One of the characteristic mechanics of Nethack (and Rogue, and similar games in the same roguelike genre) is its identification system. At the beginning of the game, only items in your starting inventory are "formally identified." The vast majority of other objects start out unknown; for example, a "shield of reflection" will initially display as a "polished silver shield" before it's identified.

A "polished silver shield" can only be a shield of reflection, but this has interesting consequences in two other cases.

  1. Some items are different from each other, but have the same "appearance." For example, if you find a "gray stone," it could be one of four things: A flint stone (useless), a touchstone (can be useful), a loadstone (which will severely encumber you because it weighs a ton and you can't drop it), or a luckstone (extremely helpful, almost necessary for winning the game).

  2. Many items (scrolls, wands, rings, spellbooks, some armor, etc.) have a randomized appearance. What this means is that there is a set list of possible apperances, say, potions could have; for example, [golden potion, swirly potion, fizzy potion, purple-red potion, etc.]. These appearances are then randomly assigned to what they actually are (potion of healing, potion of paralysis, potion of see invisible, potion of polymorph, etc.).

    Which means that a hexagonal amulet could save your life in one game (amulet of life saving), and choke you to death the next (amulet of strangulation).

Naturally, this makes identifying items a critical part of the game. Items can be "formally identified," meaning that they will unambiguously show up as definitely being a certain item (ex. all the jeweled wands you find will show up as wands of create monster); this is done primarily via scrolls or spellbooks of identify. Typically those are in short supply, though, which brings us to...

Informal identification. This means that you're pretty sure (or certain) that a certain unidentified item is of a certain type (or that it can only be one of several types), but you haven't "formally" identified it yet. This can be done via several methods: engrave-testing for wands, sink-testing for rings, or, the most common method...

scroll price ID chart

... price identification! Which is what this challenge is about.

In a nutshell, there are shops located throughout the Dungeons of Doom (yes, the shopkeepers thought it'd be a good idea to set up shop in some underground dungeon; don't ask why). In these shops, you can buy and sell the various items that you come across during your travels. When buying or selling an item, the shopkeeper will first tell you how much he would sell it to you / buy it from you for. Since certain items are guaranteed to have specific prices, you can use this to informally identify a certain type of item.

Some items, such as the scroll of light, are the only items to cost a certain amount, which allows you to unambiguously identify them; however, most items share a price group with other items of the same class, which only lets you narrow down the possibilities (which is still useful). However, the buy/sell prices of an item are affected by a number of variables (such as your Charisma stat). Hence the chart above.

Could you tell I like Nethack?

Input

Input will be provided as a (vanilla, 3.4.3) Nethack game currently being played:

"For you, most gracious sir; only 177 for this scroll labeled VERR YED HORRE."
--More--

        ------------
        |          .                                        ---------
        |          |                         ----------     |    ^  |
        |          .##       ################.        +#   #.       .#
        |          | #                       |       _|#   #---------#
        |          | ###                   ##.<       |#   ####      #
        |          .#########################----------#      #      #
        ------------   ###     #         ############# #      #      #
           #             #  -----------  #             #      #   ####
         ###             ###|         |###             #      #   #----------
         #                 #.         |#             ### #    #   #|.???????|
        ##                  |         |#             #--------#   #|.??@????|
    ----.----###############.         |#             #|      |#   #-@???????|
    |.......+#              |         |#             #.      |#    ----------
    |.......|               |         .#              |      |#
    |......>|               -----------               |      +#
    ---------                                         --------


Wizard the Evoker         St:12 Dx:14 Co:11 In:16 Wi:12 Ch:10  Chaotic
Dlvl:2  $:0  HP:11(11) Pw:0(8) AC:9  Exp:1 T:11

This means that it is guaranteed to have several properties:

  • It will always be 24 lines long.

  • Each line will always be 80 characters or less in length.

  • The second-to-last line will consist of the following "tokens": the player's name and title (in the form of "foo the bar"), the list of attributes (separated by a single space), and the player's alignment (Lawful, Neutral, or Chaotic). Each token will be separated by a variable number of spaces.1

  • The list of attributes will always be St:* Dx:* Co:* In:* Wi:* Ch:*, where a * character represents an integer from 3 to 25.2 (The point of interest here is the last stat, Charisma, which you need to calculate prices.)

  • The first line will always consist of a shop-related message (specifically, the message that is displayed when you are buying or selling an item). Furthermore, this item is guaranteed to be a single, unidentified, unnamed scroll. For buying an item, this is:

    "For you, {TITLE}; only {PRICE} for this scroll labeled {LABEL}."--More--
    

    and for selling, it is:

    {SHK} offers {PRICE} gold pieces for your scroll labeled {LABEL}.  Sell it? [ynaq] (y)
    

    where the "variables" listed in {curly braces} are the following:

    • {TITLE} is always one of "good", "honored", "most gracious", or "esteemed", concatenated with either "lady" or "sir".

    • {PRICE} is always an integer.

    • {LABEL} will always be one of the following (source):

      ZELGO MER       JUYED AWK YACC  NR 9             XIXAXA XOXAXA XUXAXA
      PRATYAVAYAH     DAIYEN FOOELS   LEP GEX VEN ZEA  PRIRUTSENIE
      ELBIB YLOH      VERR YED HORRE  VENZAR BORGAVVE  THARR
      YUM YUM         KERNOD WEL      ELAM EBOW        DUAM XNAHT
      ANDOVA BEGARIN  KIRJE           VE FORBRYDERNE   HACKEM MUCHE
      VELOX NEB       FOOBIE BLETCH   TEMOV            GARVEN DEH
      READ ME
      
    • {SHK} will always be one of the following (source):

      Skibbereen      Ballingeary     Inishbofin      Annootok        Abitibi
      Kanturk         Kilgarvan       Kesh            Upernavik       Maganasipi
      Rath Luirc      Cahersiveen     Hebiwerie       Angmagssalik    Akureyri
      Ennistymon      Glenbeigh       Possogroenoe    Aklavik         Kopasker
      Lahinch         Kilmihil        Asidonhopo      Inuvik          Budereyri
      Kinnegad        Kiltamagh       Manlobbi        Tuktoyaktuk     Akranes
      Lugnaquillia    Droichead Atha  Adjama          Chicoutimi      Bordeyri
      Enniscorthy     Inniscrone      Pakka Pakka     Ouiatchouane    Holmavik
      Gweebarra       Clonegal        Kabalebo        Chibougamau     Lucrezia
      Kittamagh       Lisnaskea       Wonotobo        Matagami        Dirk
      Nenagh          Culdaff         Akalapi         Kipawa
      Sneem           Dunfanaghy      Sipaliwini      Kinojevis
      

    This message may be split onto another line (but it will never take up more than 2 lines).3

  • Aside from the first few lines, all bets are off as to what the rest of the screen looks like. Nethack uses the majority of the ASCII character set. The only thing that you can safely assume is that the input will be purely ASCII (however this probably won't matter because you can discard lines 3-22 anyway).

If the input is taken as a function argument, it will be given exactly as shown in the example above (newline separated). If you input via STDIN, it will be given as 24 consecutive lines of input (again, as shown above). You may choose whether you want the input to have a trailing newline or not. The input is guaranteed to have no trailing spaces.

Output

Output should be provided as what I should #name the scroll that I just price-ID'd. The naming system I use (and that I have seen others use) is:

  • If the scroll is unambiguously identified as a certain scroll (identify, light, enchant weapon), #name it that. This is the case for scrolls of the following base prices (you will see how to calculate base price below): 20 -> identify, 50 -> light, 60 -> enchant weapon.

  • Otherwise, take the first three letters of the scroll's appearance, or the first word if it is less than 3 characters. For example, ZELGO MER becomes ZEL, VE FORBRYDERNE becomes VE, etc. Concatenate with this (a space, and then) the base price of the scroll. For example, ELB 300.

  • If the base price can be one of two possibilities, I usually keep trying to buy or sell the item until I get an offered price that unambiguously places it into a certain price slot. However, you can't do that in this challenge, so just separate the two possible base prices with a slash (/). For example, HAC 60/80.

Here's the formula for converting the base price of an item into the price you are offered to buy it:

  • start with the base price of the item

  • chance of a possible 33% "unidentified surcharge," calculated via price += price / 3

  • another chance of a 33% "sucker markup" (this isn't random chance actually, but for the purposes of this challenge it is), calculated the same way

  • a charisma modifier, which is applied as follows:

    Ch    3-5     6-7       8-10      11-15  16-17     18        19-25
    Mod   +100%   +50%      +33%      +0%    -25%      -33%      -50%
    Code  p *= 2  p += p/2  p += p/3  ---    p -= p/4  p -= p/3  p /= 2
    

And here's the formula for base price -> sell price:

  • start with the base price of the item

  • divide this by either 2 or 3 ("normal" or "sucker markup" respectively; again, not random, but it is for the purposes of this challenge)

  • chance of a further 25% reduction4, calculated via price -= price / 4

The division is integer division, which means the result at each step is rounded down. (Source: wiki, and a bit of source code digging. Reversing these formulas is your job.)

Finally, here's a handy-dandy ASCII chart that shows the possible buy prices (grouped by Charisma stat) and sell prices of a scroll with a certain base price:

Base  Ch<6          6-7          8-10         11-15        16-17        18           19-25        Sell
20    40/52/68      30/39/51     26/34/45     20/26/34     15/20/26     14/18/23     10/13/17     5/6/8/10
50    100/132/176   75/99/132    66/88/117    50/66/88     38/50/66     34/44/59     25/33/44     12/16/19/25
60    120/160/212   90/120/159   80/106/141   60/80/106    45/60/80     40/54/71     30/40/53     15/20/23/30
80    160/212/282   120/159/211  106/141/188  80/106/141   60/80/106    54/71/94     40/53/70     20/26/30/40
100   200/266/354   150/199/265  133/177/236  100/133/177  75/100/133   67/89/118    50/66/88     25/33/38/50
200   400/532/708   300/399/531  266/354/472  200/266/354  150/200/266  134/178/236  100/133/177  50/66/75/100
300   600/800/1066  450/600/799  400/533/710  300/400/533  225/300/400  200/267/356  150/200/266  75/100/113/150

(This is identical to the chart on the wiki except that it lists all possible sell prices, while the wiki chart neglects to include two of the four possible sell prices. No, I didn't manually make that chart; generated with this Ruby script.)

Test cases

Input:

"For you, honored sir; only 80 for this scroll labeled LEP GEX VEN ZEA."
--More--                   #                                          #
                      ----------------                              -----
                      |              |              ------------####+   |
      -----           |              -##############+          .#   |   |
      |   .###########|           >  |#           # |          |  ##.   |
      |   |          #------------.---#           ##.          |  # -----
      -+---          ##################             ----.-------###    #
       ####                     ###                     #       #      #
          #                     #                       #     ###      ###
          ###                 ###                       #     #          #
            #                 #                         #   ###     -----|--
       -----.---            ###                     ----+---#       |...@..|
       |       |            #                       |      |#       |???+??|
       |  <    .#          ##                     ##+      |        |+?????|
       |       |#    ------.-------                 |      |        |??]?@?|
       ---------###  |            |                 |      |        --------
            #     #  |            |                 --------
                  ###|            |                       #
                    #+            |
                     --------------

Wizard the Evoker         St:11 Dx:15 Co:9 In:20 Wi:9 Ch:11  Chaotic
Dlvl:7  $:0  HP:11(11) Pw:1(8) AC:9  Exp:1

Output: LEP 60/80


Input:

"For you, most gracious sir; only 80 for this scroll labeled DAIYEN FOOELS."
--More--                                                             #
                ------------                         -----      -------
 -----          |          |                         |   |      |     |
 |!)%|          |          |     ---------------     |   |     #-     |
 |*[@|          |          .#####|   <         |#####.   |   ###|     |
 |?(?|          ---------.--    #+             |#    |   |   #  |     |
 |[!(|                  ##       |             |#    |   +#### #.     .#
 |.@.|         ##################.             +#    ---.-     #|     |#
 ---|-                ###        ---------------#       ##     #-------#
    ##                #                    ######        #     #       #
     #              ###                         #        #     #       #
     ##             #                           #        #     #       #
  ------        #####                           #        #     #       #
  |    |       -.----                           #        #     #       #
  |    .#####  |^   |                        ####        #     #       #
  |    |    #  |    |          ----          #-----------.---- #       #------
  |    |    ###|    |          |  |          #.      >       | #       #|    |
  ------      #.    |          |  |           |              .##       #|    |
               |    |          ----           |              |         #.    |
               ------                         ----------------          ------

Wizard the Evoker         St:11 Dx:14 Co:16 In:15 Wi:10 Ch:9  Chaotic
Dlvl:6  $:0  HP:11(11) Pw:9(9) AC:9  Exp:1

Output: enchant weapon


Input:

Aklavik offers 113 gold pieces for your scroll labeled GARVEN DEH.  Sell it?
[ynaq] (y)

     -----      ------                                 ---------      -------
     |   |      |    |                         #     ##.       |      |.?)%/|
     |   |    ##.    |                       -----   # |       |      |.@!=*|
     |<  |    # |    |            #        ##.   .#####+    >  |#    #-.*?@[|
     |   .##### |    |      ------------   # | { |#    |^      |#    #|.=%)+|
     ---.-      |    |      |          .#### |   |#    ---------##   #-------
       ##       -.----     #.          |     |   |#          # ###   #
       #         ########  #|          .##   |   |#            ##    #
     ###                #  #------------ #   -----#          ####    #
     #                  #######          ##########################  #
     #                    #   #                            ###----.--#
     #                    ### #                            # #|     |#
   --.----       ########################################### #.     |#
   |     |       #----------.-#                               |     |#
   |     |       #|          |#                               -------
   |     |       #|          .#
   |     |########|          |
   -------        ------------
                   #    #
Wizard the Evoker         St:9 Dx:14 Co:11 In:19 Wi:10 Ch:12  Chaotic
Dlvl:4  $:0  HP:11(11) Pw:5(9) AC:9  Exp:1 Satiated

Output: GAR 300


Input:

"For you, good lady; only 67 for this scroll labeled VE FORBRYDERNE."--More--

                                                 -------
                                               ##|     |
  ------------                                 # |     |
  |+[!/!?%[?)|                               ### |     |          --------
  |[)(!/+]?!@|               #               #   |     |        ##+      |
  |.......@..|            --------------   ###   | <   | ##       |      |
  --------+---           #|            |   #     |     |  #       |    > |
          #            ###|            .####     --.----  ###    #-      |
          #            ###.            |           #        #  ###|      |
          #          #### ---.----------           #        ######.      |
          #          ####    ##                    #         ###  --------
          #        ####       #                    #         #
          #        ####       ########################     ###
        ###      ####                            ----+---- #
        #   #    ####                            |       .##
    ----.------####                              |  ^    |
    |         +####                              |    >  |
    |         |                                  | ^     |
    -----------                                  ---------

Wizard the Evoker         St:18 Dx:18 Co:16 In:20 Wi:20 Ch:18  Chaotic
Dlvl:4  $:150 HP:11(11) Pw:5(7) AC:9  Exp:1

Output: VE 100


Input:

Droichead Atha offers 5 gold pieces for your scroll labeled XIXAXA XOXAXA
XUXAXA.  Sell it? [ynaq] (y)
                                                ------------
                   -----                        |          .#
                   |   .### -----------        #.       {  |#
       -----       |   |  # |         |      ###|          |#
       |   .#     #.   |  # |         |      #  ---------+--#
       |   |    ###-|---    |         .##  ###          ##  #
       |   |    #   # #     |         | #  #            #   #
       |   -#####   #       |         | #### ############   #
       |>  | ##     #       ---------+-  ## -.----------    # ----------
       |   .####    ###             ## #####|          |    # |.*??/?)*|
       -----   #      #             #  #    |          |    # |@*)%!)]%|
               ###    ###         ######    |          |    # |.=)!%*!!|
                 #      #         #  #      |          |    ##+@*[%)(%?|
                 #####################      |          |      |.]?*?)%%|
                    -----+---.----##########.          |      |.%)%!!!%|
                    |            +##        ------------      ----------
                    |    <       |                  #
                    |            |
                    --------------

Wizard the Digger          St:11 Dx:9 Co:14 In:6 Wi:6 Ch:9  Lawful
Dlvl:3  $:0  HP:15(15) Pw:0(1) AC:9  Exp:1

Output: identify

(I had to manually compile Nethack with all the other shopkeeper names removed because I couldn't find a shopkeeper who had a space in his name...)

Rules


1: this isn't necessarily always true during a Nethack game, but we assume this for the sake of simplicity.

2: again, not always true. Strength can be 18/01 through 18/**, but you don't need to handle that.

3: more gross oversimplifications. For example, it is possible for a shopkeeper to call you "scum" or "most renowned and sacred creature," but you don't have to handle any of that.

4: which a smart player gets around by repeatedly offering to sell the item until given the higher price.

Doorknob

Posted 2015-07-04T15:28:14.173

Reputation: 68 138

15Holy wall of text! – orlp – 2015-07-04T15:51:45.943

7

@orlp "Also that has got to be one of the longest challenges I've written" :D

– Doorknob – 2015-07-04T15:52:31.347

Answers

10

Javascript (ES6), 1610 724 601 612 419 405 390 bytes

a=>(b=a.match(/(\d+) (g|f).+d (\w{0,3})[\s\S]+h:(\d+)/),c=+b[4],d=[20,50,60,80,100,200,300].reduce((d,e)=>(f=e/2,g=~~(e/3),(b[2]=='g'?[g-(g>>2),g,f-(f>>2),f]:[e,e+g,e+g+~~((e+g)/3)].map(h=>c<6?h*2:c<8?h+h>>1:c<11?h+~~(h/3):c<16?h:c<18?h-(h>>2):c<19?h-~~(h/3):h>>1)).includes(+b[1])?[...d,e]:d),[]),i={20:'identify',50:'light',60:'enchant weapon'}[d[0]],j=b[3]+' '+d[0],d[1]?j+'/'+d[1]:i||j)

Large wall of text, meet large wall of code.

Ungolfed

inp => (
    extraction = inp.match(/(\d+) (g|f).+d (\w{0,3})[\s\S]+h:(\d+)/),

    charisma = +extraction[4],

    allowed = [20, 50, 60, 80, 100, 200, 300].reduce((allowed, base) => (
        tmp1 = base / 2,
        tmp2 = ~~(base / 3),
        (extraction[2] == 'g' ?
              [tmp2 - (tmp2 >> 2), tmp2, tmp1 - (tmp1 >> 2), tmp1]
          :
              [base, base + tmp2, base + tmp2 + ~~((base + tmp2) / 3)].map(val =>
                  charisma < 6 ?
                      val * 2
                  : charisma < 8 ?
                      val + val >> 1
                  : charisma < 11 ?
                      val + ~~(val / 3)
                  : charisma < 16 ?
                      val
                  : charisma < 18 ?
                      val - (val >> 2)
                  : charisma < 19 ?
                      val - ~~(val / 3)
                  : val >> 1
          )).includes(+extraction[1]) ? [...allowed, base] : allowed
    ), []),

    name_ = {
        20: 'identify',
        50: 'light',
        60: 'enchant weapon'
    }[allowed[0]],

    tmp3 = extraction[3] + ' ' + allowed[0],

    allowed[1] ?
        tmp3 + '/' + allowed[1]
    :
        name_ || tmp3
)

Example

document.getElementById('input').value = `Droichead Atha offers 5 gold pieces for your scroll labeled XIXAXA XOXAXA
XUXAXA.  Sell it? [ynaq] (y)
                                            ------------
               -----                        |          .#
               |   .### -----------        #.       {  |#
   -----       |   |  # |         |      ###|          |#
   |   .#     #.   |  # |         |      #  ---------+--#
   |   |    ###-|---    |         .##  ###          ##  #
   |   |    #   # #     |         | #  #            #   #
   |   -#####   #       |         | #### ############   #
   |>  | ##     #       ---------+-  ## -.----------    # ----------
   |   .####    ###             ## #####|          |    # |.*??/?)*|
   -----   #      #             #  #    |          |    # |@*)%!)]%|
           ###    ###         ######    |          |    # |.=)!%*!!|
             #      #         #  #      |          |    ##+@*[%)(%?|
             #####################      |          |      |.]?*?)%%|
                -----+---.----##########.          |      |.%)%!!!%|
                |            +##        ------------      ----------
                |    <       |                  #
                |            |
                --------------

Wizard the Digger          St:11 Dx:9 Co:14 In:6 Wi:6 Ch:9  Lawful
Dlvl:3  $:0  HP:15(15) Pw:0(1) AC:9  Exp:1`;

func=a=>(b=a.match(/(\d+) (g|f).+d (\w{0,3})[\s\S]+h:(\d+)/),c=+b[4],d=[20,50,60,80,100,200,300].reduce((d,e)=>(f=e/2,g=~~(e/3),(b[2]=='g'?[g-(g>>2),g,f-(f>>2),f]:[e,e+g,e+g+~~((e+g)/3)].map(h=>c<6?h*2:c<8?h+h>>1:c<11?h+~~(h/3):c<16?h:c<18?h-(h>>2):c<19?h-~~(h/3):h>>1)).includes(+b[1])?[...d,e]:d),[]),i={20:'identify',50:'light',60:'enchant weapon'}[d[0]],j=b[3]+' '+d[0],d.length-1?j+'/'+d[1]:i||j);

function run() {
  try {
    document.getElementById('output').value = func(document.getElementById('input').value);
  } catch (e) {
    document.getElementById('output').value = 'ERROR!';
  }
}
<textarea id="input" placeholder="input" rows="20" cols="70"></textarea>
<br>
<button onclick="run()">Run</button>
<br>
<textarea id="output" placeholder="output" disabled="disabled"></textarea>

usandfriends

Posted 2015-07-04T15:28:14.173

Reputation: 687

1That is one big pile of script. – Fatalize – 2015-11-30T10:35:02.610

1Woah, someone finally answered this? Nice :D – Doorknob – 2015-11-30T12:29:20.730

@Doorknob Been wanting to update this for a while, finally got around to it. – usandfriends – 2019-03-13T20:08:48.450