Catch The Rabbit Game: How to Create Non-Repeating Randomization? (JavaScript, CSS, HTML)

I've designed this game, it's my first project. It's a spin-off from "The Pig Game" in a JavaScript course. I tweaked the HTML and CSS templates of the Pig Game for the UI, but I did the game design and coding from scratch. You can play the game here: https://jeffparadox.000webhostapp.com/

I've got some questions, if anyone cares:

1. What do you think, do you see any problems? Can anything be clearer (especially in terms of UI) than it is now?

2. I use math.random for flipping images randomly. But sometimes the same image comes twice in a row, which gives more time to the players than they suppose to have. As a work around, I randomized the number twice, but that's not a complete solution. I've researched some solutions to this but they were varied and a bit complicated. It feels like there must be a simpler, more elegant solution to this problem. Any advice or direction is greatly appreciated:

// Declare game engine variables
let interval, imageNumber;

...

// Loop with time intervals
interval = setInterval(function () {
// Genarate image number
imageNumber = Math.trunc(Math.random() * 10);
// Show image with the generated number
animalEl.src = animal-${imageNumber}.jpg; isSameNumber = imageNumber; } else { imageNumber = Math.trunc(Math.random() * 10); animalEl.src = animal-${imageNumber}.jpg;
}
}, 500);

1. Here's my complete JS code. If anyone's interested in checking it and telling me which parts I can clean-up and optimize, I'd really appreciate it. Thanks in advance:
"use strict";

// Selecting elements
const player0El = document.querySelector(".player--0");
const player1El = document.querySelector(".player--1");
const tries0El = document.getElementById("tries--0");
const tries1El = document.getElementById("tries--1");
const current0El = document.getElementById("current--0");
const current1El = document.getElementById("current--1");

const animalEl = document.querySelector(".animal");
const btnSpin = document.querySelector(".btn--spin");
const btnReset = document.querySelector(".btn--reset");
const btnRestart = document.querySelector(".btn--restart");

const youWin0El = document.querySelector("#you-win--0");
const youWin1El = document.querySelector("#you-win--1");

const highScore0El = document.querySelector(".high-score--0");
const highScore1El = document.querySelector(".high-score--1");

// Declare let variables
let triesLeft,
playerScores,
highScores,
activePlayer,
round,
currentScore,
playing;

// Starting conditions
const init = function () {
youWin1El.textContent = "You Win! π";
youWin0El.textContent = "You Win! π";
currentScore = 0;
triesLeft = [10, 10];
playerScores = [0, 0];
highScores = [0, 0];
activePlayer = 0;
round = 3;
playing = true;

btnRestart.textContent = π ROUND: ${round}; tries0El.textContent = 10; tries1El.textContent = 10; current0El.textContent = 0; current1El.textContent = 0; animalEl.src = "noAnimal.jpg"; player0El.classList.remove("player--winner"); player1El.classList.remove("player--winner"); player0El.classList.add("player--active"); player1El.classList.remove("player--active"); }; // Initialize game init(); // ***GAME FUNCTIONS*** // Switch players const switchPlayer = function () { activePlayer = activePlayer === 0 ? 1 : 0; player0El.classList.toggle("player--active"); player1El.classList.toggle("player--active"); }; // Check how many rounds left const checkRound = function () { btnRestart.textContent = π ROUND:${round};
if (round < 1) {
gameOver();
} else if (triesLeft[activePlayer] < 1 && round > 0) {
if (triesLeft[0] === 0 && triesLeft[1] === 0) {
triesLeft[0] = 10;
triesLeft[1] = 10;
tries0El.textContent = 10;
tries1El.textContent = 10;
}
switchPlayer();
}
};

// End of game
const gameOver = function () {
playing = false;
if (playerScores[0] > playerScores[1]) {
youWin0El.classList.remove("hidden");
} else if (playerScores[0] < playerScores[1]) {
youWin1El.classList.remove("hidden");
} else if (playerScores[0] === playerScores[1]) {
youWin1El.textContent = "It's a Tie π²";
youWin0El.textContent = "It's a Tie π³";
youWin1El.classList.remove("hidden");
youWin0El.classList.remove("hidden");
}
};

// Check the rabbit, increase and log the score
const checkRabbit = function () {
if (imageNumber === 0) {
currentScore =
Number(document.getElementById(current--${activePlayer}).textContent) + 1; playerScores[activePlayer] = currentScore; document.getElementById( current--${activePlayer}
).textContent = currentScore;
}
};

// Update tries left
const triesUpdate = function () {
triesLeft[activePlayer] -= 1;
document.getElementById(tries--${activePlayer}).textContent = triesLeft[activePlayer]; }; // Update high scores const registerHighScore = function () { if (playerScores[activePlayer] > highScores[activePlayer]) { highScores[activePlayer] = playerScores[activePlayer]; document.getElementById( high-score--${activePlayer}
).textContent = High Score: ${highScores[activePlayer]}; } }; // ***GAME ENGINE*** // Declare game engine variables let interval, imageNumber; let isSameNumber = 10; // Spinning images btnSpin.addEventListener("click", function () { if (playing) { // Change button to Stop btnSpin.textContent = β STOP!; // Stop the spinning (Runs only when interval is declared) if (interval) { clearInterval(interval); interval = null; btnSpin.textContent = π° SPIN!; triesUpdate(); checkRabbit(); registerHighScore(); if (triesLeft[0] < 1 && triesLeft[1] < 1) { round -= 1; } checkRound(); // Start the spinning (Runs only when interval is null or undefined) } else { // Loop with time intervals interval = setInterval(function () { // Genarate image number imageNumber = Math.trunc(Math.random() * 10); if (imageNumber !== isSameNumber) { // Show image with the generated number animalEl.src = animal-${imageNumber}.jpg;
} else {
imageNumber = Math.trunc(Math.random() * 10);
animalEl.src = animal-\${imageNumber}.jpg;
}
}, 500);
}
}
});

// ***RESET GAME***


First of all, great job on the game! Even though it's a simple concept it feels really well realized by you.

Let's go over your questions one by one:

1. UI & UX

Well done on this one! Simple, clean design, sufficient spacing on elements, consistent styling, nice visual appeal. Obviously not optimized for mobile devices, but that's a plus for the future. Just a few notes (note that these are stylistic and opinionated):

• Consistent colouring: Most of the website is held in a green-ish tone, which makes the "tries left" counter and label stick out. This is nice for the actual counter, since it's supposed to be prominent, but the label looks a bit out of place with that much attention dragged to it. My proposal would be to give it a darkish-green colour, since it's just supposed to be recognized once, at the moment when the user wonders what the big, red "10" is supposed to mean.
• Rounded containers: All the elements on the site have rounded corners, so the image of the animals should probably be rounded aswell, even if just slightly.
• Text alignment: Since the emojis in the button texts are larger than the actual characters themself, the button text looks like it's drifting off to the bottom, while a centered alignment may look more natural.
• Element focus: A thick, bright border drags the attention of the user on it. While the score counter surely is important, I wouldn't say that it has to be there constantly. Maybe just on the player who is currently in turn? Something that I'd like to propose is that the score counter of the player who is not in turn is made slightly transparent, and that once a player scores a point, their counter briefly flashes a border, to indicate success.
• Visual feedback: As far as I can see, the button with "Round 3" on it does not actually do anything when I click on it? Could be that I'm mistaken. However, keep in mind that non-clickable buttons should rather not give any visual feedback (like transitions) when clicked or hovered, since the lack of action on interaction could be confusing to the user.

2. Randomization

Randomness is ... well, random. Or pseudo-random, atleast in the context of Math.random(). Every number has the same chance of being picked, even ones that you just had. To solve this, your solution is definitely a good start, but there is still a slim chance that the same number could be generated twice or thrice in a row. Here are some proposals on how to tackle the issue:

• Store the last number: Fairly straightforward. Whenever you show a new image, you store the index of that image in a variable. When you generate the new image, you keep on generating random numbers until you reach one that doesn't match the previous one.
let lastIndex = -1;

// Here, we want to set the new image.
let newIndex = -1;
do {
newIndex = Math.trunc(Math.random() * 10);
} while(newIndex !== lastIndex);

// newIndex is now for sure different from the last one!

• Permutations: This one is slightly more advanced. Essentially, you want to ensure that out of a range of numbers (let's say 1, 2, 3, 4, 5, 6, 7, 8, 9) every number gets picked once, before any number gets picked again. There is an answer on StackOverflow that describes this well, with code examples added. The way that it works is that the list of numbers shown above is shuffled (like you would shuffle a deck of cards), and then numbers are taken off one end one at a time. For a game this could be both wanted and unwanted, this is up to your design choices.

3. Code Review

• Data structures: Currently you are storing the data for each player (attempts, scores, ...) in two-element-arrays. You could improve clarity here by creating dedicated objects holding the data for each player. This also spares you from constantly having to index into arrays to get/set data. If you want to get really clean you could pack the UI elements into objects (or classes even!), where you can write wrapper methods that are more readable than explicitely setting classes or attributes every time.
const player1 = {
attempts: 0,
score: 0,
// ...
}

let currentPlayer = player1;

• Function definitions: In your code, you are constantly using const <functionname> = function() { ... } to create new functions. While I do not see a reason for it myself, I believe that you might have been inspired by arrow-syntax usages: const <functionname> = () => { ... }. The difference is out of scope for my answer, but personally I find a function keyword at the start of the line more readable than one nested in the middle of it.
function myFunction1() {
console.log("It's immediately apparent that I am a function!");
}
const myFunction2 = function() {
console.log("At first glance, I might be mistaken for a variable.");
}


It should be noted that with a game like this there are always architectural optimizations possible. You could start to (ab)use ES6 classes to more cleanly represent entities and data, you could switch to a more state-machine based game-loop, but for a project like yours what you have is good enouugh already.

Bottom line: Really well done on the project, and I hope that my feedback does not discourage or overwhelm you. Most of the things mentioned are opinionated, and there's surely someone out there who totally disagrees with me on some. If you have any questions, don't be afraid to ask, I'd be glad to clarify!

• Oh wow! Thank you so much for all this feedback and taking your time for it! I'll go over all of these word by word and clean up the project. Cheers! – Treves Studios Mar 29 at 16:32

Setup:

1. Create an array (or database table) of all the values.
2. Shuffle that array.

Then, In your loop, pick the 'next' item.

And, of course, decide what to do when you run out of items. -- perhaps reshuffle and restart. Perhaps simply restart.