Code to generate an array of 364 random special characters with 20 random words broken up throughout
I am trying to recreate a Fallout 4 style terminal hacking game, so I wanted to create code to generate the terminal output (namely, 32 rows with 12 characters each and 20 random words spread out). Everything in this output array is being treated as a separate character and then output onto the page. The words need to be broken up and the array must be flat. I feel okay about this code but I am curious if there is a better way to achieve this output code, and then mainly improve the event listener at the bottom as well as the logic for adding "data-password"
// INFO + CONTEXT
// Terminals in the Fallout francise include a gameplay mechanic/minigame "Hacking" to unlock the terminals. The game has similariies to the board game "Mastermind",
// Terminals work similarly across Fallout 3, Fallout New Vegas, Fallout 4 and Fallout 76 - for this project I am basing the terminal off of the Fallout 4 game.
// In game, terminals have two columns which each have 16 rows of 12 characters. Each row always starts with a randomised hexadecimal number.
// These rows are mainly made up of random special characters, with potential passwords scattered throughout
// The number and length of the potential passwords is dependant on the characters intelligence statistic and the difficulty level of the terminal
// The password length is capped at 12 and the frequency at 20. Generally speaking the higher the intelligence, the fewer potential passwords and the shorter they are
// You get four attempts to guess the password and each guess returns a "Likeness" output, similar to Wordle, which is a number that refers to the number of letters that align with the correct password
// Finding complete sets of brackets within the garble on one row either removes incorrect/dud words or resets your attempts
// Successfully hacking a terminal may allow one to: access information, disable or enable turrets or spotlights, alarm systems, and various other defenses or traps, open locked doors or safes.
// More information about terminals can be found here: https://fallout.fandom.com/wiki/Hacking_(Fallout_4)
import { generate } from "random-words";
import { rng, find, findMany } from "./helpers";
const SPECIAL_CHARACTERS = [
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"-",
"_",
"[",
"]",
"{",
"}",
"<",
">",
"|",
"'",
";",
":",
"/",
"?",
",",
".",
";",
];
const PASSWORD_COUNT = 20;
const PASSWORD_LENGTH = 5;
const CHARS_PER_ROW = 12;
const TOTAL_ROWS = 32;
const TOTAL_CHARS = CHARS_PER_ROW * TOTAL_ROWS;
const cypher = find("#cypher");
// 32 * 12 = 384 total characters filled with random garble from special characters
const output = Array.from(
{ length: TOTAL_CHARS },
() => SPECIAL_CHARACTERS[rng(SPECIAL_CHARACTERS.length)],
);
// Generate a list of 20 random 5 letter words for password options
const potentialPasswords = generate({
exactly: PASSWORD_COUNT,
minLength: PASSWORD_LENGTH,
maxLength: PASSWORD_LENGTH,
});
// Generate a list of 20 starting indexes for the password positions within the output array
const passwordPositions = [];
while (passwordPositions.length < PASSWORD_COUNT) {
// Ensure that a password doesn't start within the final indexes and cannot be full added to the array without overflow
const randomValue = rng(TOTAL_CHARS - PASSWORD_LENGTH);
// Prevent duplicate indexes
if (passwordPositions.includes(randomValue)) continue;
// Ensure that no password has an overlapping index
if (
passwordPositions.every(
(value) => Math.abs(value - randomValue) >= PASSWORD_LENGTH + 1,
)
) {
passwordPositions.push(randomValue);
}
}
// Using the randomly generated password positions with the randomly generated dictionary words, insert each char of the password seperately starting at the index by deleting some characters
for (let i = 0; i < potentialPasswords.length; i++) {
for (let j = 0; j < PASSWORD_LENGTH; j++) {
output.splice(passwordPositions[i] + j, 1, potentialPasswords[i][j]);
}
}
let previousSeenPassword;
// Loop over the array in 12 character chunks for each row
for (let i = 0; i < output.length; i += CHARS_PER_ROW) {
// Extract the chunk
const chunk = output.slice(i, i + CHARS_PER_ROW);
// Add each individual character of a row to a new dividing element
const row = document.createElement("div");
// For each character in chunk, create a span element for the row
for (let j = 0; j < chunk.length; j++) {
const letter = document.createElement("span");
letter.innerHTML = chunk[j];
if (!SPECIAL_CHARACTERS.includes(chunk[j])) {
// Using a previously seen password tracker as the incrementing of j will mean the password position is undefined except for the first letter
const password = potentialPasswords[passwordPositions.indexOf(i + j)];
if (password) previousSeenPassword = password;
// Set attribute for grouping and event listeners
letter.setAttribute("data-password", password || previousSeenPassword);
}
row.appendChild(letter);
}
cypher.appendChild(row);
}
const passwords = Array.from(findMany("[data-password]"));
cypher.addEventListener("mouseover", (e) => {
if (passwords.includes(e.target)) {
const selector = e.target.getAttribute("data-password");
const adjacentSpans = Array.from(findMany(`[data-password=${selector}]`));
adjacentSpans.forEach((span) => {
span.classList.add("bg-black", "text-white");
});
console.log(adjacentSpans);
}
});
1 Answer 1
An optimization for generating PASSWORD_COUNT
password positions is to (i) remove the known spacing, (ii) sample from the reduced values, and (iii) add back the spacing. This improves performance by removing the need to check for overlapping indices and avoids unresolvable situations (e.g. if you need to fit two words of length 5 into 11 characters, it's impossible to proceed if the first word is placed right in the middle). Also, note that if the words are very dense, it's faster to kick out the reduced values that aren't used than to select reduced values.
// gets n distinct random values in [0, max)
// where max, n are unsigned integers
const multipleRNG = (max, n) => {
if (n < 0)
throw Error(`Assertion (${n} >= 0) failed for n!`)
if (max < n)
throw Error(`Cannot get ${n} distinct values from ${max} options!`);
// easier to kick out (max - n) values than to select n values
if (2 * n > max) {
const exclude = multipleRNG(max, max - n);
const values = [];
for (let i = 0; i < max; ++i)
if (!exclude.includes(i))
values.push(i);
return values;
}
const values = [];
while (values.length < n) {
const randomValue = rng(max);
if (!values.includes(randomValue))
values.push(randomValue);
}
return values;
};
// returns n distinct values in [0, max) such that
// 1. i + space <= max for all values i
// 2. i + space < j for distinct values i < j
// where space is a positive integer
const spacedRNG = (max, n, space) => {
if (space < 1)
throw Error(`Assertion (${space} > 0) failed for space!`)
const values = multipleRNG(max - n * space + 1, n);
values.sort(function (a, b) {
return a - b;
});
for (let i = 0; i < n; ++i)
values[i] += i * space;
return values; // note: sorted
};
const passwordPositions = spacedRNG(
TOTAL_CHARS,
PASSWORD_COUNT,
PASSWORD_LENGTH
);
Testing with PASSWORD_COUNT = 62
:
saved>metal{
grade$&>}:/s
kill)stuck^e
arly*_thumb^
smoke;found/
world$;piece
)ranch{carry
;noise{.smel
l-south:alon
e|strip;mode
l?]above,sco
re<start)glo
be&house'wro
ng-board(dri
ve;stove-gue
ss/giant?qui
ck/since&#be
ing/check#ag
ree%serve^br
eak@@thumb.s
labs,point^c
arry'price$/
;given,score
$smell(cause
<route]heart
$short]''%*]
!quiet:queen
!:?!aloud@wh
ich;vapor!ch
art'?slabs|h
urry{party;&
spell],birds
Otherwise, I think the code is quite well-written. Great work on making a reproducible MVP!
Update
For even better performance, you can use the Durstenfeld shuffle:
// returns the values of [0, max) in random order
// where max is an unsigned integer (not checked)
// see: https://bost.ocks.org/mike/shuffle/
const fancyShuffle = (max) => {
const values = [];
for (let i = 0; i < max; ++i)
values.push(i);
for (let m = 0; m < max; ++m) {
const i = rng(m);
const tmp = values[m];
values[m] = values[i];
values[i] = tmp;
}
return values;
};
// gets n distinct random values in [0, max)
// where max, n are unsigned integers
const multipleRNG = (max, n) => {
if (n < 0)
throw Error(`Assertion (${n} >= 0) failed for n!`)
if (max < n)
throw Error(`Cannot get ${n} distinct values from ${max} options!`);
return fancyShuffle(max).slice(0, n);
};
This ensures the generation of positions in O(max)
time.
-
\$\begingroup\$ For this Fisher-Yates or Durstenfeld shuffle, how does this code guarantee no duplicate values and maintain the spacing which I need? \$\endgroup\$Dalton– Dalton2024年11月24日 09:06:32 +00:00Commented Nov 24, 2024 at 9:06
./helpers
to the question? I'm not sure whatrng, find, findMany
are supposed to do. \$\endgroup\$export const findMany = (selector, context = document) => context.querySelectorAll(selector); export const rng = (max) => Math.floor(Math.random() * max) export const find = (selector, context = document) => context.querySelector(selector);
\$\endgroup\$