tmedwards / sugarcube-2

SugarCube is a free (gratis and libre) story format for Twine/Twee.
https://www.motoslave.net/sugarcube/2/
BSD 2-Clause "Simplified" License
185 stars 42 forks source link

random walk in debug mode #188

Open felix9 opened 2 years ago

felix9 commented 2 years ago

For a story I'm working on, I implemented a simple random-walk control in debug mode.

Does this seem like something that's generally useful? I've included the implementation below, and I'd be happy to make a PR to integrate it into Sugarcube-2 (and/or -3).

One subtlety of the implementation: It tries to do a shuffle, not uniform random, so in principle you could use walk/back to do a complete depth-first traversal of deterministic story graphs.

(This will probably need tweaking if the story has text inputs that expect to read comma or period.)

random-walk.css ```css .random-walk { bottom: 100px; display: flex; flex-direction: column; font-size: 18px; gap: 8px; position: fixed; right: 4px; } .random-walk a { background-color: #eee; border: 1px solid transparent; border-radius: 4px; color: #000; cursor: pointer; opacity: 0.3; padding: 2px 4px; } .random-walk a:hover { background-color: #333; border-color: #eee; color: #eee; } .random-walk-chosen { outline: 1px solid #9cc; } ```
random-walk.js ```js /** Random walk */ function randomWalkSetup() { if (!Config.debug) return; const back = () => { const chosen = $('#passages .random-walk-chosen'); if (chosen.length > 0) { chosen.removeClass('random-walk-chosen'); } else { Engine.backward(); } }; const visited = new Set(); const lastPick = {}; const id = el => [$(el).attr('data-passage'), $(el).text()].join('|'); const walk = () => { const chosen = $('#passages .random-walk-chosen'); if (chosen.length === 1) { chosen.click(); return; } chosen.removeClass('random-walk-chosen'); const here = State.passage; const all = $('#passages a[data-passage]'); if (all.length === 0) return; let avail = all.filter((i, el) => !visited.has(id(el))); if (avail.length === 0) { all.each((i, el) => visited.delete(id(el))); avail = all; if (avail.length !== 1) { avail = avail.filter((i, el) => id(el) !== lastPick[here]); } } const i = random(avail.length - 1); $(avail[i]).addClass('random-walk-chosen'); avail[i].scrollIntoView(); lastPick[here] = id(avail[i]); visited.add(lastPick[here]); }; $(document).on(":passageend", () => { $(".random-walk").remove(); const div = $("
").appendTo("#story"); $('back') .on("click", back).appendTo(div); $('walk') .on("click", walk).appendTo(div); }); $(document).on("keydown", ev => { switch (ev.key) { case ",": back(); break; case ".": walk(); break; } }); } randomWalkSetup(); ```
AceiusIO commented 2 years ago

I think this would be a useful addition. Although is a back button really nessacary? We already have a way to do that in the debug menu with the turns counter.

Also, during testing I discovered the walk button was covered up by the debug menu if it's pulled out. Quick fix:

  $(document).on(":passageend", () => {
    $(".random-walk").remove();
    const div = $("<div class=random-walk>").appendTo("#story");
    $('<a class=random-walk-go title="<period> forward">Walk</a>')
      .on("click", walk).appendTo(div);
    $('<a class=random-walk-back title="<comma> backward">Back</a>')
      .on("click", back).appendTo(div);
  });

This does sacrifice visibility of the back button, but again, you can do that with the turns dropdown.

felix9 commented 2 years ago

"back" here isn't exactly the same as "previous turn", because "walk" is two steps. First press of "walk" will select a link, second "walk" will go to that link. If you "walk", then "back", it will un-select the link, rather than going to the previous turn, which is a quick way of choosing a different link.

Equivalently, you could "walk, walk, back", so I guess it isn't really necessary to have a special-case back, but I like the light-weight way of changing the random choice.

Alternatively, it could be a "reject this choice, choose another" button, but that feels more complicated to use: three controls instead of two.

I think if something like this were integrated into Sugarcube, I'd put it in the history controls, something like this:

I'd have to play with it a bit, but that seems to me like it would be fluid and unsurprising.