Debugger for beta testers & authors/devs (records and plays-back games, cheat, undo)

Instead of getting any work done on my story, I instead spent tons of hours making this.

The Code, nickname: Lizard Debug
javascript:{ /*Lizard Debug for ChoiceScript by @Twiger_Fluff | version 1*/ /*press H to get recording output | J to playback | K to print variables to the console*/ var NumberOfStepsToNotDo = 0; var tag = document.createElement("script"); tag.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"; tag.onload = function() { $(document).on('click', 'button', function () { P_ButtonClicked(this); }); $(document).on('click', 'a', function () { P_LinkClicked(this); }); $(document).on('submit', function () { P_Submit(); }); $(document).on('keypress', function (event) { P_Keypress(event); }); }; document.getElementsByTagName("head")[0].appendChild(tag); var P_data = []; /*true = show stats, false = hide stats, -1 = [non-option next] press, 0-99999 is option picked and next pressed, "string" is text or number input*/ var data_P = []; var P_i = 0; function P_ButtonClicked(button) { if (button.getAttribute('class') == "next") { if (!document.getElementById("menuButton").innerHTML.includes("Settings")) { /*on settings page*/ return; } else { if (document.getElementsByClassName("choice").length > 0) { /*is a choice*/ P_data.push(Number($("input[type='radio'][name='choice']:checked").val())); } else { P_data.push(-1); } } } else if (button.getAttribute('id') == "statsButton") { if (button.innerHTML.includes("Show")) { P_data.push(true); } else if (button.innerHTML.includes("Return")) { P_data.push(false); } } else if (button.getAttribute('id') == "menuButton" && !button.innerHTML.includes("Settings") && document.getElementById("statsButton").innerHTML.includes("return")) { /*return to main game and stats was open normally but closed from settings page*/ P_data.push(false); } } function P_LinkClicked(linkele) { if (linkele.getAttribute('id') == "alertify-ok") { /*game restarted*/ P_data = []; data_P = []; } } function P_Submit() { /*is email, textbox, or number*/ let button = document.getElementsByClassName("next")[0]; if (document.getElementsByName("email").length > 0) { /*do nothing as it is followed by a "next" regardless of proper sumbit or not*/ } else { P_data.push(document.getElementsByName("text")[0].value); /*this will (on purpose) include invalid inputs*/ } } function P_Keypress(event) { if (event.charCode == 104) /*h*/ { prompt("Copy to clipboard: Ctrl+C, Enter", JSON.stringify(P_data.concat(window.getSelection().toString()))); } if (event.charCode == 106) /*j*/ { P_i = 0; data_P = JSON.parse(prompt("Paste from clipboard: Ctrl+V, Enter", "")); DoLizard(); } if (event.charCode == 107) /*k*/ { console.log(window.stats); } } async function DoLizard() { if (P_i < (data_P.length - (NumberOfStepsToNotDo + 1))) { DoStep(data_P[P_i]); window.setTimeout(WaitForLoad, 200); } else if (P_i == data_P.length - 1) { find(data_P[data_P.length - 1]); } P_i++; } function WaitForLoad() { if (document.body.getAttribute("class") != "frozen") { DoLizard(); } else { window.setTimeout(WaitForLoad, 200); } } function DoStep(step) { if (step === false) { if (document.getElementById("statsButton").innerHTML.includes("Return")) { document.getElementById("statsButton").click(); } else { console.warn("Step couldn't be played back properly.") } } else if (step === true) { if (document.getElementById("statsButton").innerHTML.includes("Show")) { document.getElementById("statsButton").click(); } else { console.warn("Step couldn't be played back properly.") } } else if (typeof(step) == "string") { document.getElementsByName("text")[0].value = step; let list = document.getElementsByClassName("next"); list[list.length - 1].click(); } else if (typeof(step) == "number") { if (step > -1) { document.getElementById(step).checked = true; } let list = document.getElementsByClassName("next"); list[list.length - 1].click(); } else { console.warn("Step couldn't be played back properly.") } } };void(0);

The main purpose of this is to make it so authors can play back a beta tester’s run through of their game, in order to find bugs or see where a spelling error is (screenshots aren’t a real thing).

Things you can do with it

Load/Save games, it just… takes a minute
Record games to be played back later at a high speed (disable page animations beforehand to make it even faster)
Highlight text as you save the play-back to make that text be highlighted for whoever plays it back. (great for pointing out grammar/spelling problems, but it actually only highlights the first instance of that string on the page)
Cheat/Undo by editing the recording or changing NumberOfStepsToNotDo in the code
Other things, probably.

How-To

After loading the code (see part-two), it will automatically start recording your choices (both in game and on the stats screen, including text/number input).
Press H to export your recording. (copy)
Press J to load and play a recording. (paste)
Press K to print all the game’s variables in the browser’s console.

How-To Part-Two

How/Where you can use the code:

  1. ran from the browser console (by reader)
  2. ran from the url bar (by reader) (but it will need “javascript:” inserted manually at the front)
  3. ran from a *script in the game (by author/dev)
  4. ran as a bookmarklet (by reader) (save the code as the URL of a bookmark and click that bookmark to run the code)
Disclaimers/Info

—> Data is wiped on game reset; both data and the code is wiped on reset so you’ll have to load the code again if it’s not done automatically.
—> There might be bugs. It needs more people testing it.
—> Not tested for/no explicit support for multiple choices on a single page
—> Only tested on Dashingdon
—> Will be broken by loading a save from smPlugin.js first, unless the person playing back the recording loads from the same save too
—> Doesn’t record settings changes or email submission

Leave some love :hugs:

Edit → apparently there is a bug where having page animations off might cause a problem with text-input when playing back. (The game still seems to continue though). → I’ll fix this when I feel motivated. For now just make sure animations are on

23 Likes

Ok, where have you been hiding all this time? Now you show up and dump one amazing trick after another :slight_smile:

This is really nice, and with a very simple approach. Well done! Do you have a working demo somewhere? Also, can you share the code in a more readable way?

This would have been very handy for the replays in RacetraCS. Suggestion: could autoplay be triggered by a URL?

4 Likes

Well I had good tricks when I was here 2 years ago too.
No need for a demo, just choose any game on dashing don, run the code, and play. After a while, press H and save, then restart the game and press J to paste the save and run it.

URL/code to autoplay (you still need to input the save string, but that can be changed)
javascript:{ /*Lizard Debug for ChoiceScript by @Twiger_Fluff | version 1*/ /*press H to get recording output | J to playback | K to print variables to the console*/ var NumberOfStepsToNotDo = 0; var tag = document.createElement("script"); tag.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"; tag.onload = function() { $(document).on('click', 'button', function () { P_ButtonClicked(this); }); $(document).on('click', 'a', function () { P_LinkClicked(this); }); $(document).on('submit', function () { P_Submit(); }); $(document).on('keypress', function (event) { P_Keypress(event); }); P_i = 0; data_P = JSON.parse(prompt("Paste from clipboard: Ctrl+V, Enter", "")); DoLizard(); }; document.getElementsByTagName("head")[0].appendChild(tag); var P_data = []; /*true = show stats, false = hide stats, -1 = [non-option next] press, 0-99999 is option picked and next pressed, "string" is text or number input*/ var data_P = []; var P_i = 0; function P_ButtonClicked(button) { if (button.getAttribute('class') == "next") { if (!document.getElementById("menuButton").innerHTML.includes("Settings")) { /*on settings page*/ return; } else { if (document.getElementsByClassName("choice").length > 0) { /*is a choice*/ P_data.push(Number($("input[type='radio'][name='choice']:checked").val())); } else { P_data.push(-1); } } } else if (button.getAttribute('id') == "statsButton") { if (button.innerHTML.includes("Show")) { P_data.push(true); } else if (button.innerHTML.includes("Return")) { P_data.push(false); } } else if (button.getAttribute('id') == "menuButton" && !button.innerHTML.includes("Settings") && document.getElementById("statsButton").innerHTML.includes("return")) { /*return to main game and stats was open normally but closed from settings page*/ P_data.push(false); } } function P_LinkClicked(linkele) { if (linkele.getAttribute('id') == "alertify-ok") { /*game restarted*/ P_data = []; data_P = []; } } function P_Submit() { /*is email, textbox, or number*/ let button = document.getElementsByClassName("next")[0]; if (document.getElementsByName("email").length > 0) { /*do nothing as it is followed by a "next" regardless of proper sumbit or not*/ } else { P_data.push(document.getElementsByName("text")[0].value); /*this will (on purpose) include invalid inputs*/ } } function P_Keypress(event) { if (event.charCode == 104) /*h*/ { prompt("Copy to clipboard: Ctrl+C, Enter", JSON.stringify(P_data.concat(window.getSelection().toString()))); } if (event.charCode == 106) /*j*/ { P_i = 0; data_P = JSON.parse(prompt("Paste from clipboard: Ctrl+V, Enter", "")); DoLizard(); } if (event.charCode == 107) /*k*/ { console.log(window.stats); } } async function DoLizard() { if (P_i < (data_P.length - (NumberOfStepsToNotDo + 1))) { DoStep(data_P[P_i]); window.setTimeout(WaitForLoad, 200); } else if (P_i == data_P.length - 1) { find(data_P[data_P.length - 1]); } P_i++; } function WaitForLoad() { if (document.body.getAttribute("class") != "frozen") { DoLizard(); } else { window.setTimeout(WaitForLoad, 200); } } function DoStep(step) { if (step === false) { if (document.getElementById("statsButton").innerHTML.includes("Return")) { document.getElementById("statsButton").click(); } else { console.warn("Step couldn't be played back properly.") } } else if (step === true) { if (document.getElementById("statsButton").innerHTML.includes("Show")) { document.getElementById("statsButton").click(); } else { console.warn("Step couldn't be played back properly.") } } else if (typeof(step) == "string") { document.getElementsByName("text")[0].value = step; let list = document.getElementsByClassName("next"); list[list.length - 1].click(); } else if (typeof(step) == "number") { if (step > -1) { document.getElementById(step).checked = true; } let list = document.getElementsByClassName("next"); list[list.length - 1].click(); } else { console.warn("Step couldn't be played back properly.") } } };void(0);

As for sharing the code in a more readable way, just paste any hard-to-read code here and click the button or press ctrl-enter

5 Likes

This is just great. A way I want to use this idea is to create a list of tests for my game, and run them in sequence with your tool (I would need a way to programmatically trigger the restart). It will be something like a random test, but more controlled - closer to integration tests.

Imagine something like:

mygameurl/?test=game1Rgame2Rgame3R

where game1 game2 are replay sequences, and R would reset the game.

1 Like

Okay, this is super cool. I love the idea of having a series of tests for my game.