Towards automating the Password Game

The Password Game is a recently viral game that allows you to choose a password subject to many challenging rules. Since it is very difficult to solve by hand, I wrote a program to do so. I haven’t automated all the parts, but I’ve gotten fairly close.

FIGURE 1 haha yes

1 Overall design

My code is here (it’s very janky and hacky so please don’t judge too harshly):

github

To use, you can paste the code into the console, and then repeatedly run apply() (while modifying the apply function in between runs if needed). I know, it’s not the best user experience.

The solution works in three phases:

  1. Generate a string that satisfies the main chunks of the password, such as the captcha, chess puzzle solution, Paul, and so on.
  2. Checksums, such as digits summing up to a certain number.
  3. Formatting, by applying the appropriate font, size, and bold/italicization to each character.

As such, I will organize this blog post in these three phases and not necessarily go by order of the rules.

The password function generates the main content, and then adds the checksums. Then the format function adds formatting, and the result gets put to the text input as HTML.

function apply() {
  let div = document.querySelector("div.ProseMirror");
  let p = div.querySelector("p");
  let pass = password("Ne7+", country, "tract", "youtu.be/EAQ2Q5uS0Gs");
  unused(pass);
  pass = format(pass);
  p.innerHTML = pass;
}

function password(chess, country, wordle, youtube) {
  let string = [
    captcha(),
    chess,
    country,
    wordle,
    youtube,
    color(),
    "pepsi",
    "may",
    "1310:00",
    "iamloved",
    moon,
    strength,
  ].join(delim);
  string += delim + roman(string);
  let elements = element(string);
  let out = `${elements}${delim}${paul}${delim}${string}${delim}${number(
    string
  )}`;
  const correction =
    5 - paul.length + 8 - moon.length + 3 - strength.length + 7; // not sure where the 7 comes from but whatever
  out += delim.repeat(131 - (out.length + correction));
  return out;
}

2 Simple rules

The first few rules are pretty trivial and any password will already have fulfilled them, so there is no need to write specific code.

Rule 1 Your password must be at least 5 characters.
Rule 2 Your password must include a number.
Rule 3 Your password must include an uppercase letter.
Rule 4 Your password must include a special character.

3 Main content

Rule 6 Your password must include a month of the year.

The shortest month is May so I added may to the solution.

Rule 7 Your password must include a roman numeral.

This is handled by the code for Rule 9.

Rule 8 Your password must include one of our sponsors.

Pepsi and Shell are shorter than Starbucks, so they are preferable. I went with Pepsi because they sponsored my dad’s colledge education.

Rule 10 Your password must include this CAPTCHA.

The funny thing is that the CAPTCHA’s filename is its solution so you can solve it with simply:

function captcha() {
  try {
    let urlparts = document.querySelector(".captcha-img").src.split("/");
    return urlparts[urlparts.length - 1].split(".")[0];
  } catch (error) {
    return "";
  }
}

However, if the numbers in the CAPTCHA are too large, they can pose problems with digits adding up to 25. To give yourself as much headroom as possible, keep rerolling until the digits sum to less than 5. There are in fact some that have no digits whatsoever.

Fortunately, the CAPTCHAs do not contain capital letters, so they will not interfere with atomic elements.

Rule 11 Your password must include today’s Wordle answer.

This one is pretty annoying to automate. I have just been solving it once per day. TODO…

Rule 12 Your password must include a two letter symbol from the periodic table.

This is already handled later on in the Checksums section.

Rule 13 Your password must include the current phase of the moon as an emoji.

Technically there are APIs that let you calculate the phase but for now we can just include all phases.

const moon = "🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘";
Rule 14 Your password must include the name of this country.

From the URL of the iframe element, the location is encoded in a scheme similar to this StackOverflow answer. In particular, I found that the latitude and longitude are encoded by keys 1d and 2d. Using the latitude and longitude, we can then use the Google Maps reverse geocoding API to get the country.

The geocodeAddress function was partially written by ChatGPT.

For ease of implementation, I just store the result in a global variable country.

async function geocodeAddress(lat, lng) {
  const apiKey = "YOUR_API_KEY"; // Replace with your Google Maps API key
  const geocodingEndpoint = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${apiKey}`;

  try {
    const response = await fetch(geocodingEndpoint);
    const data = await response.json();

    if (data.status === "OK") {
      const results = data.results[0];
      console.log(results);
      // Process the geocoding results here
      for (let component of results.address_components) {
        if (component.types[0] == "country") {
          country = component.long_name.toLowerCase();
        }
      }
    } else {
      // Handle the geocoding error
      console.error("Geocoding failed:", data.status);
    }
  } catch (error) {
    // Handle any other errors that may occur
    console.error("An error occurred:", error);
  }
}

function geoguessr() {
  let data = document.querySelector("iframe.geo").src.split("?pb=");
  data = data[data.length - 1].split("!");
  let latitude = 0,
    longitude = 0;
  for (let val of data) {
    let header = val.substr(0, 2);
    if (header == "1d") {
      latitude = parseFloat(val.substr(2));
    } else if (header == "2d") {
      longitude = parseFloat(val.substr(2));
    }
  }
  geocodeAddress(latitude, longitude);
}
Rule 15 Your password must include a leap year.

This one is pretty easy since 0 is a leap year.

Rule 16 Your password must include the best move in algebraic chess notation.

The chess puzzle is an SVG file. If you inspect the source code of the file, you’ll see that it starts with an ASCII description of the position.

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny" viewBox="0 0 390 390"><desc><pre>r q . . . r k .
. . . n . p p .
p b . . . . n .
. . . N . . P .
. p B . Q P . .
. . . . B . . .
P P . . . . . .
. . K R . . . R</pre></desc><defs>

This means that we can programmatically convert it to Forsyth-Edwards Notation without having to resort to computer vision techniques such as chessvision.ai.

The following functions, partially written by ChatGPT, perform the conversion:

async function fetchSVGAndConvertToFEN(url, isWhiteTurn) {
  try {
    // Fetch the SVG file
    const response = await fetch(url);
    const svgText = await response.text();

    // Extract the ASCII representation from the SVG source code
    const asciiRegex = /<pre>(.*?)<\/pre>/s;
    const match = svgText.match(asciiRegex);
    if (!match) {
      throw new Error("ASCII representation not found in the SVG source code");
    }

    const asciiPosition = match[1];

    // Convert the ASCII position to FEN
    const fen = asciiToFEN(asciiPosition, isWhiteTurn);
    return fen;
  } catch (error) {
    console.error("Error fetching the SVG file:", error);
  }
}

function asciiToFEN(asciiPosition, isWhiteTurn) {
  const ranks = asciiPosition.trim().split("\n");
  let fen = "";

  for (let rank of ranks) {
    let fenRank = "";
    let emptySquares = 0;
    let splitrank = rank.split(" ");

    for (let char of splitrank) {
      if (char === ".") {
        emptySquares++;
      } else {
        if (emptySquares > 0) {
          fenRank += emptySquares;
          emptySquares = 0;
        }
        fenRank += char;
      }
    }

    if (emptySquares > 0) {
      fenRank += emptySquares;
    }

    fen += fenRank + "/";
  }

  fen = fen.slice(0, -1); // Remove trailing slash

  fen += isWhiteTurn ? " w" : " b";
  fen += " - 0 1"; // Assuming no information about castling rights and move counters

  return fen;
}

Afterwards, you can solve it using a chess engine. All the puzzles are mate in 2, so you don’t have to search very deep and chess engines should be able to solve them instantly. For example, you can use a Javascript version of the strongest chess engine, stockfish.js.

In practice, I haven’t managed to get it working yet due to some CORS issues but I’ll figure it out eventually haha.

function chess() {
  const svgURL = document.querySelector(".chess-img").src;
  const turn = document.querySelector(".chess-wrapper").querySelector(".move");
  const isWhiteTurn = turn.innerHTML.split("White").length > 1;

  fetchSVGAndConvertToFEN(svgURL, isWhiteTurn)
    .then((fen) => {
      console.log("Forsyth-Edwards Notation (FEN):", fen);
      let stockfish = new Worker(
        "https://cdn.jsdelivr.net/npm/[email protected]/src/stockfish.min.js"
      );
      stockfish.postMessage(`position fen ${fen}`);
      stockfish.postMessage("go depth 8");
      stockfish.onmessage = (e) => {
        console.log(e.data);
      };
    })
    .catch((error) => {
      console.error("Error:", error);
    });
}
Rule 17 🥚 ← This is my chicken Paul. He hasn’t hatched yet, please put him in your password and keep him safe.
Rule 23 Paul has hatched! Please don’t forget to feed him, he eats three 🐛 every minute.

Like many others, I found keeping Paul alive the most stressful and I died many times due to deleting him, overfeeding him, or starving him.

The funny thing is that you can have both a chicken and egg in the password and it will satisfy both these rules. After hatching, Paul will stay alive as long as:

To keep him alive, we can use a setInterval to repeatedly reset Paul.

const paul = "🐔🐛🐛🐛🥚";

function feed_paul() {
  let div = document.querySelector("div.ProseMirror");
  let p = div.querySelector("p");

  let string = p.innerHTML;
  if (string.length == 0) {
    return;
  }

  let parts = string.split(delim);
  parts[1] = paul;
  p.innerHTML = parts.join(delim);
}

window.setInterval(feed_paul, 20000);
Rule 21 Your password is not strong enough 🏋️‍♂️

Pretty trivial.

const strength = "🏋️‍♂️🏋️‍♂️🏋️‍♂️";
Rule 22 Your password must contain one of the following affirmations: I am loved, I am worthy, I am enough

The first one is the shortest so I just put iamloved in my password.

Rule 24 Your password must include the URL of a 5 minute 13 second long YouTube video.

This one is a little tricky to automate. If you get a really long YouTube video for which very few exist, then you might just be out of luck.

You can google for “0:00 / 5:13 youtube” or “00:00 / 10:00 youtube” if two digits.

Make sure the URL does not contain “X”, “L”, “C”, “D”, “M”, or more than one “V”, or else the roman numeral checksum will be ruined.

Use the shortened youtu.be link for a more compact password.

Technically, you can use the free tier of serpapi to automate this but I haven’t gotten to implementing it yet.

Rule 28 Your password must include this color in hex.

This is fairly straightforward since the color is set in CSS. Again, ChatGPT helped with implementation.

function extractBackgroundColor(string) {
  // Extract the RGB values from the string
  const rgbValues = string.match(/\d+/g);

  if (!rgbValues || rgbValues.length !== 3) {
    return null; // Invalid string format
  }

  // Convert the RGB values to hexadecimal
  const hexValues = rgbValues.map((value) => {
    const hex = parseInt(value).toString(16);
    return hex.length === 1 ? "0" + hex : hex;
  });

  // Construct the hexadecimal color string
  const hexColor = "#" + hexValues.join("");
  console.log("hexcolor", hexColor);

  return hexColor;
}

function color() {
  let div = document.querySelector("div.rand-color");
  if (div) {
    return extractBackgroundColor(div.style.backgroundColor);
  } else {
    return "";
  }
}

Some hex colors will result in overflowing some checksums, such as digits adding up to 25, so just reroll until it is suitable.

Rule 35 Your password must include the current time.

This one is super annoying to do without cheating since usually the time will overflow some checksums, unless you are very patient and wait until the opportune time.

To cheat, I just overwrote the window.Date object:

window.Date = function () {
  this.now = function () {
    return "10:00";
  };

  this.toLocaleString = function (locale, x) {
    return "10:00 PM";
  };
};

window.Date.now = function () {
  return "10:00";
};

Hacking the window.Date object also allows solving of the Wordle, by the way, but that feels rather icky.

4 Checksums

Rule 5 The digits in your password must add up to 25.

This can be accomplished by adding up the digits in the password and appending extra digits to pad the difference between the sum and 25.

function number(string) {
  const digits = string.match(/\d/g);
  let sum = 0;

  if (digits !== null) {
    for (let i = 0; i < digits.length; i++) {
      sum += parseInt(digits[i]);
    }
  }
  let remainder = 25 - sum;
  let number_check = "";
  while (remainder > 0) {
    let num = Math.min(remainder, 9);
    number_check += num;
    remainder -= num;
  }
  return number_check;
}
Rule 9 The roman numerals in your password should mutiply to 35.

Since 35 is the product of prime numbers 5 and 7, the only solutions are:

Notably, this eliminates letters such as L, D, C, and M, which is extremely annoying later on when finding a YouTube video of the appropriate length.

My janky code simply checks if there is a V and sets the roman numeral checksum to VII if so, and XXXV otherwise.

Technically, both a V and VII could be “naturally occurring” in, for example, a YouTube link, but it is quite unlikely.

function roman(string) {
  if (string.match(/V/g) !== null) {
    return "VII";
  } else {
    return "XXXV";
  }
}
Rule 18 The elements in your password must have atomic numbers that add up to 200.

We can iterate over all elements and count how many times they occur. When counting, it is important to realize that some elements are substrings of other, for example fluorine (F) is a substring of iron (Fe), so when counting F, you need to subtract the number of occurrences of Fe.

There are more efficient ways of implementing but since there are only a hundred or so elements, it is not a big deal.

After counting, we can greedily add some “safe” elements (which do not contain any roman numerals, and preferably single letter elements) from a whitelist to ensure that they sum up to 200.

Oh, and if there are no two-letter elements (for Rule 12), we add a Helium (He) before proceeding.

function element(string) {
  const elements = [
    "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Ni", "Co", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og"];
  let e_weight = 200;
  let has_two_letter = false;
  for (let i in elements) {
    let count = string.split(elements[i]).length - 1;

    if (count == 0) {
      continue;
    }
    if (elements[i].length > 1) {
      has_two_letter = true;
    }
    for (let j in elements) {
      if (i == j) {
        continue;
      }
      let count2 = elements[j].split(elements[i]).length - 1;
      let count3 = string.split(elements[j]).length - 1;
      if (count2 > 0) {
        count -= count3;
      }
    }
    e_weight -= (parseInt(i) + 1) * count;
    console.log(elements[i], parseInt(i) + 1, count, e_weight);
  }
  let element_check = "";
  if (!has_two_letter) {
    element_check = "He";
    e_weight -= 2;
  }
  let padders = [50, 16, 15, 9, 8, 7, 5, 2, 1];
  for (let i in padders) {
    while (e_weight >= padders[i]) {
      element_check += elements[padders[i] - 1];
      e_weight -= padders[i];
    }
  }
  return element_check;
}
Rule 32 Your password must include the length of your password.
Rule 33 The length of your password must be a prime number.

These two rules can be solved together. First, pick a reasonable prime number that gives you sufficient headroom (111, 113, 131, 211), without overflowing the checksums and giving you sufficiently many characters to work with. Then, add that number to your solution. Afterwards, pad with some special characters such as |.

I found that my Javascript string.length produces different results from the password game, presumably due to certain Unicode characters, so I added a constant correction amount that seems to work.

Just for fun, I combined this with the “current time” one (which is hardcoded to 10:00), by adding “1310:00” to my solution.

5 Formatting rules

Rule 19 All the vowels in your password must be bolded.
Rule 26 Your password must contain twice as many italic characters as bold.

The strategy is:

Rule 27 At least 30% of your password must be in the Wingdings font.
Rule 29 All roman numerals must be in Times New Roman.

The strategy is:

Rule 30 The font size of every digit must be equal to its square.
Rule 31 Every instance of the same letter must have a different font size.

The strategy is:

function format(string) {
  let outputHTML = "";
  let n_vowels = 0;

  let sizes = [0, 1, 4, 9, 12, 16, 25, 28, 32, 36, 42, 49, 64, 81];
  let letters = {};

  for (let i = 0; i < string.length; i++) {
    const char = string[i];

    const is_digit = /\d/.test(char);
    if (is_digit) {
      n = parseInt(char);
      outputHTML += `<span style="font-family: Wingdings; font-size: ${
        n * n
      }px"><em>${char}</em></span>`;
    } else {
      let bar = "";
      const is_vowel = /[aeiouy]/i.test(char);
      const is_letter = /[a-z]/i.test(char);
      const is_roman = /[XVI]/.test(char);
      let wtf = 1;
      if (is_letter) {
        lowerchar = char.toLowerCase();
        letters[lowerchar] = (letters[lowerchar] || 0) + 1;
        wtf = sizes[letters[lowerchar]];
        console.log(char, wtf);
      }

      const wingdings = `<span style="font-family: Wingdings; font-size: ${wtf}px">`;

      if (is_vowel) {
        if (is_roman) {
          const times = `<span style="font-family: Times New Roman; font-size: ${wtf}px">`;
          outputHTML += `${times}<strong>${char}</strong></span>`;
        } else {
          outputHTML += `${wingdings}<strong>${char}</strong></span>`;
        }
        n_vowels += 1;
      } else {
        if (is_roman) {
          const times = `<span style="font-family: Times New Roman; font-size: ${wtf}px">`;
          outputHTML += `${times}<em>${char}</em></span>`;
        } else {
          outputHTML += `${wingdings}<em>${char}</em></span>`;
        }
      }
    }
  }
  console.log(n_vowels, string.length);
  let deficit = n_vowels * 3 - string.length;
  if (deficit > 0) {
    outputHTML += `<em>${"!".repeat(deficit)}</em>`;
  }
  console.log("formatting...");
  console.log(string);
  console.log(outputHTML);
  return outputHTML;
}

I spent a while racking my brain over how to “efficiently” use the minimal number of tags but I gave up and just formatted each character one at a time.

6 Random rules

Rule 20 Oh no! Your password is on fire. Quick, put it out!

Simply rerun the script to regenerate the password lol.

Rule 25 A sacrifice must be made. Pick 2 letters that you will no longer be able to use.

I wrote a function to check which letters are unused and print them to the console.

function unused(string) {
  const count = {};
  const lowercaseString = string.toLowerCase();
  for (let i = 0; i < lowercaseString.length; i++) {
    const character = lowercaseString[i];

    if (/[a-z]/i.test(character)) {
      count[character] = (count[character] || 0) + 1;
    }
  }
  abc = "abcdefghijklmnopqrstuvwxyz";
  for (let i in abc) {
    if (!count[abc[i]]) {
      console.log(abc[i], "unused");
    }
  }
}

Usually, letters Z and K are unused.

Rule … 36?? Please re-type your password before the bomb explodes

To avoid dying to the bomb, I set a setInterval to copy the password to the re-type field every second.

function bomb() {
  let divs = document.querySelectorAll("div.ProseMirror");
  if (divs.length < 2) {
    return;
  }

  let p0 = divs[0].querySelector("p");
  let p1 = divs[1].querySelector("p");
  p1.innerHTML = p0.innerHTML;
}
window.setInterval(bomb, 1000);