Silly way to do page numbers (Quest gamebook)

Edit: Stupid typo (a - for a =)
Edit2: Another silly mistake that didn't show up on my first test

Something recently prompted me to look at some of the gamebooks on my bookshelf, and I remembered that you were often turning dozens of pages at once. They never had a link to the next page; maybe because that would encourage cheating, or maybe so you didn't accidentally see a possible next option when flipping through pages.

Now, that's not necessary in an online gamebook. But maybe having page numbers jumping all over the place gives you a feeling of nostalgia or something. So I made a script for Quest that will assign every page a number; attempting to maximise the number of pages that each link jumps over.

To use this code:

  • The page the player starts on will always be page 1
  • If you want a page to have a specific number (for example if you have "when you solved the riddle, multiply the answer by 3 and turn to that page number" type puzzles) you can give it a pagenumber int attribute.
    • If you set pagenumber but also set number_static to false, the page will start where you specify but can still move. This could be used to give the algorithm a hint about what would be a good order, so it starts faster.
      • Or you could set one of the pages to be 999… and then it will scatter your pages over the range 1-999 even if there's spaces in the middle (I think… haven't tested that)
  • If you put the text ?#? in a page's text part, it will be replaced with the page number.
    • If you don't, the page number will be added to the beginning of the page
      • If you want to suppress the page number, set the attribute number_shown to true
  • In the normal links, the text ?#? will be replaced by the number of the page it goes to.
    • If it's not included, it will append (turn to page ?#?) to each option… like in the gamebooks on my bookshelf
  • There is a limit set on how many attempts it will make to get the best order for the pages. I've set it to three times the number of pages, but I doubt it'll ever get that high.
    • If it starts timing out on big gamebooks, search for the expression maxnumber*3 and change it to something lower. This may result in some pages being close to their successors, but will finish quicker.
    • If you want to see how long it's taking, look at the javascript console …

This code can go in the enter script, on the game's "Scripts" tab.

firsttime {
  allpages = AllObjects()
  list remove (allpages, player)
  maxnumber = ListCount (allpages)
  next_number = 1
  page_names_by_number = NewStringDictionary()
  if (not HasInt (player.parent, "pagenumber")) {
    player.parent.pagenumber = 1
  }
  foreach (page, allpages) {
    if (HasInt (page, "pagenumber")) {
      if (not HasBoolean (page, "number_static")) {
        page.number_static = true
      }
      if (page.number_static and page.pagenumber > maxnumber) {
        maxnumber = page.pagenumber
      }
    }
    else {
      while (FindWithAttribute (allpages, "pagenumber", next_number)) {
        next_number = next_number + 1
      }
      page.pagenumber = next_number
    }
    dictionary add (page_names_by_number, ""+page.pagenumber, page.name)
    if (page.pagenumber > maxnumber) {
      maxnumber = page.pagenumber
    }
    if (not HasAttribute (page, "bi_links")) {
      page.bi_links = NewObjectList()
    }
    if (HasAttribute (page, "options")) {
      foreach (name, page.options) {
        target = GetObject (name)
        if (not target = null) {
          if (not HasAttribute (target, "bi_links")) {
            target.bi_links = NewObjectList()
          }
          if (not ListContains (page.bi_links, target)) {
            list add (page.bi_links, target)
          }
          if (not ListContains (target.bi_links, page)) {
            list add (target.bi_links, page)
          }
        }
      }
    }
  }
  orders_tried = NewStringList()
  currentorder = ""
  while (not ListContains (orders_tried, currentorder) and ListCount (orders_tried) < maxnumber*3) {
    list add (orders_tried, currentorder)
    currentorder = ""
    for (i, 1, maxnumber) {
      currentorder = currentorder + ">"
      if (DictionaryContains (page_names_by_number, ""+i) and GetBoolean (GetObject (StringDictionaryItem (page_names_by_number, ""+i)), "number_static")) {
        j = maxnumber + 1
      }
      else {
        j = i + 1
      }
      while (DictionaryContains (page_names_by_number, ""+j) and GetBoolean (GetObject (StringDictionaryItem (page_names_by_number, ""+j)), "number_static")) {
        j = j + 1
      }
      if (j <= maxnumber) {
        pushswap = 0
        leftpage = null
        rightpage = null
        if (DictionaryContains (page_names_by_number, ""+i)) {
          leftpage = GetObject (StringDictionaryItem (page_names_by_number, ""+i))
        }
        if (not leftpage = null) {
          foreach (comp, leftpage.bi_links) {
            if (comp.pagenumber < i) {
              diff = i - comp.pagenumber
              while (diff < maxnumber+12) {
                pushswap = pushswap + 1
                diff = diff * 3/2
                if (diff = 1) {
                  diff = 2
                }
              }
            }
            else if (comp.pagenumber > j) {
              diff = comp.pagenumber - j
              while (diff < maxnumber+12) {
                pushswap = pushswap - 1
                diff = diff * 3/2
                if (diff = 1) {
                  diff = 2
                }
              }
            }
          }
        }
        if (DictionaryContains (page_names_by_number, ""+j)) {
          rightpage = GetObject (StringDictionaryItem (page_names_by_number, ""+j))
        }
        if (not rightpage = null) {
          foreach (comp, rightpage.bi_links) {
            if (comp.pagenumber < i) {
              diff = i - comp.pagenumber
              while (diff < maxnumber+12) {
                pushswap = pushswap - 1
                diff = diff * 3/2
                if (diff = 1) {
                  diff = 2
                }
              }
            }
            else if (comp.pagenumber > j) {
              diff = comp.pagenumber - j
              while (diff < maxnumber+12) {
                pushswap = pushswap + 1
                diff = diff * 3/2
                if (diff = 1) {
                  diff = 2
                }
              }
            }
          }
        }
        if (pushswap > 1) {
          if (DictionaryContains (page_names_by_number, ""+i)) {
            dictionary remove (page_names_by_number, ""+i)
          }
          if (DictionaryContains (page_names_by_number, ""+(i+1))) {
            dictionary remove (page_names_by_number, ""+(i+1))
          }
          if (not leftpage = null) {
            leftpage.pagenumber = i + 1
            dictionary add (page_names_by_number, ""+(i + 1), leftpage.name)
          }
          if (not rightpage = null) {
            rightpage.pagenumber = i
            dictionary add (page_names_by_number, ""+i, rightpage.name)
          }
        }
      }
      if (DictionaryContains (page_names_by_number, ""+i)) currentorder = currentorder + StringDictionaryItem (page_names_by_number, ""+i)
    }
    JS.console.log ("Arranged order: "+currentorder)
  }
  // Apply the new numbers to the pages
  foreach (page, AllObjects()) {
    if (HasString (page, "description") and HasString (page, "pagenumber")) {
      if (Instr (page.description, "?#?") > 0 and not HasBoolean (page, "number_shown")) {
        page.number_shown = true
      }
      page.description = Replace (page.description, "?#?", ""+page.pagenumber)
    }
    if (HasAttribute (page, "options")) {
      new_options = NewStringDictionary()
      foreach (option, page.options) {
        number = "???"
        target = GetObject (option)
        link = DictionaryItem (page.options, option)
        if (not target = null and HasInt (target, "pagenumber")) {
          if (Instr (link, "?#?") > 0) {
            link = Replace (link, "?#?", target.pagenumber)
          }
          else {
            link = link + " <small>(turn to page " + target.pagenumber + ")</small>"
          }
        }
        dictionary add (new_options, option, link)
      }
      page.options = new_options
    }
  }
}
if (HasInt (player.parent, "pagenumber") and not GetBoolean (player.parent, "number_shown")) {
  msg ("<h4 align=\"center\">Page " + player.parent.pagenumber + "</h4>")
}

You will also need this utility function:

<function name="FindWithAttribute" type="boolean" parameters="list,attr,value">
  foreach (obj, list) {
    if (HasAttribute (obj, attr)) {
      v = GetAttribute (obj, attr)
      if (TypeOf (v) = TypeOf (value)) {
        if (value = v) {
          return (true)
        }
      }
    }
  }
  return (false)
</function>

(which checks a list to see if it contains any objects with a specific attribute value)

Note: Yes, it's a spring tensor distribution algorithm implemented using bubble sort. As much as I'd like to do it more efficiently, every method I could think of ends up being way more complex thanks to the interesting quirks of Quest's data scructures.


I banged my head against the wall a few days until I made it worked, and your code turned up to be very useful.
I was expecting the page numbers to be random every time the player starts up the game, Nope!, the page numbers are consistent, making it a true nostalgic returning trip to the golden age of real game books where you actually flip the books.

I have used your code on my game and have credited mrangel.

So, for newcomers who is facing issue of adding this code, you can continue reading the following.
The code 1 of mrangel is straightforward, directly paste into gamebook, script tab as stated by mrangel.
The code 2 is the hard one, no matter how you paste it, it ain't going work.

  1. Paste it at game start script, doesn't works.
  2. Paste it at page 1, doesn't works.
  3. Paste it at function, doesn't works.

So, the answer is, right click the pages, + function.
Give it a name of FindWithAttribute
Set the return type to Boolean
Add in the parameters, list, attr, value (You should have 3 parameters in total.)

Copy the simplified code 2 into the function called FindWithAttribute.

foreach (obj, list) {
  if (HasAttribute (obj, attr)) {
    v = GetAttribute (obj, attr)
    if (TypeOf (v) = TypeOf (value)) {
      if (value = v) {
        return (true)
      }
    }
  }
}
return (false)

The code 2 is the hard one, no matter how you paste it, it ain't going work.

Ah, sorry, didn't think about giving detailed directions. If you're on the desktop editor, you can also paste it in full code view; just putting it outside of any other objects or functions; but I guess that's not something most people know how to do either.

Glad you could figure out how to use it :)


Failed to load game.
The following errors occurred:
Invalid XML: Name cannot begin with the '<' character, hexadecimal value 0x3C. Line 209, position 81.

This is not important, since the code is already working.
But just informing, that copy pasting code 2 into full code view doesn't work.
Okay, maybe I placed in wrong place.
So, I created a fake function.
Then I go to full code view and replace fake function with code 2, which still gives me the same error.


I might have found the error the first line of code 2.
The last letter should be > and not <.

<function name="FindWithAttribute" type="boolean" parameters="list,attr,value"<
  foreach (obj, list) {
    if (HasAttribute (obj, attr)) {
      v = GetAttribute (obj, attr)
      if (TypeOf (v) = TypeOf (value)) {
        if (value = v) {
          return (true)
        }
      }
    }
  }
  return (false)
</function>

This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums