my only issue so far is when the ships have the same value, I get an error message if I'm using VSC.
TypeError: Assignment to constant variable.
I was trying to get to different values by using the condition but I think it doesn't work.
let grid = [[]];
const letters = "abc".toUpperCase();
// create the Grid
const createGrid = (size) => {
let row;
let col = 0;
for (row = 0; row < size; row++) {
grid[row] = [];
for (col = 0; col < size; col++) {
grid[row][col] = `${letters[row]}${col + 1}`;
}
}
};
createGrid(letters.length);
console.table(grid);
// Start the game
//create ships
const flatArray = grid.reduce((acc, curr) => {
return [...acc, ...curr];
}, []);
function getRandomNumber(len) {
return Math.floor(Math.random() * len);
}
const randomNumber1 = getRandomNumber(flatArray.length);
const randomNumber2 = getRandomNumber(flatArray.length);
while(randomNumber1 == randomNumber2) {
getRandomNumber();
}
var shipOne = flatArray[randomNumber1];
var shipTwo = flatArray[randomNumber2];
console.log(shipOne);
console.log(shipTwo);
Fix an infinite loop
I created a snippet from your example code and it runs forever in the error case you are describing.
A problem is that you have a while loop that will run forever:
while(randomNumber1 == randomNumber2) {
getRandomNumber();
}
You run this code as long as randomNumber1 and randomNumber2 are the same but you update neither of them in the loop so it can never be false.
Try this:
while(randomNumber1 == randomNumber2) {
randomNumber2 = getRandomNumber(flatArray.length);
}
You might also make your code a little smarter and remove elements from the flatArray array you have already chosen.
One other approach you could try is to create a function pickRandomEntries() that will pick N random entries from an array, without ever picking the same ones.
To do this, we shuffle a copy of your flatArray(), then pick the first two items.
This way we never have to check for the ships having the same value:
let grid = [[]];
const letters = "abc".toUpperCase();
const createGrid = (size) => {
let row;
let col = 0;
for (row = 0; row < size; row++) {
grid[row] = [];
for (col = 0; col < size; col++) {
grid[row][col] = `${letters[row]}${col + 1}`;
}
}
};
createGrid(letters.length);
const flatArray = grid.reduce((acc, curr) => {
return [...acc, ...curr];
}, []);
// Shuffle an array into a random order (using Fisher-Yates)
function shuffle(arr) {
for(let i = arr.length - 1; i > 0; i--) {
let index = Math.floor(Math.random() * (i + 1));
[arr[index], arr[i]] = [arr[i], arr[index]];
}
return arr;
}
// Pick count random items from the array
function pickRandomEntries(arr, count) {
let shuffled = shuffle([...arr]);
return shuffled.slice(0, count);
}
let [shipOne, shipTwo] = pickRandomEntries(flatArray, 2);
console.log( { shipOne, shipTwo });
Inside of your while loop if the two random numbers were the same you would be stuck inside an infinite loop. Like Peter stated.
To fix this use the code Peter stated but don't forget you still need to pass in the array length into this function. Otherwise you will have an error:
while(randomNumber1 == randomNumber2) {
randomNumber2 = getRandomNumber(flatArray.length);
}
Thanks!!
Related
I am trying to choose random unique numbers everytime when I click button. For this my function is:
const chooseNumber = () => {
var r = Math.floor(Math.random() * 75) + 1;
console.log(r)
while(selectedNumbers.indexOf(r) === -1) {
selectedNumbers.push(r);
}
console.log(selectedNumbers);
};
But the problem is if the random number is already on my list, I need to click the button again to generate new number and it goes until it find the number which is not on the list. But I want to generate number which is not on the list directly so I dont need to click the button everytime. Thanks for you helps.
You are in a right track, except the while loop should be for random number generator, not pushing number into an array:
const selectedNumbers = [];
const chooseNumber = () => {
let r;
do
{
r = Math.floor(Math.random() * 75) + 1;
}
while(selectedNumbers.indexOf(r) > -1)
selectedNumbers.push(r);
console.log(r, "["+selectedNumbers+"]");
};
<button onclick="chooseNumber()">Generate</button>
Note, that this might eventually lead to a freeze, since there is no fail safe check if array is full, so to battle that we should also check length of the array:
const selectedNumbers = [];
const maxNumber = 75;
const chooseNumber = () => {
let r;
do
{
r = ~~(Math.random() * maxNumber) + 1;
}
while(selectedNumbers.indexOf(r) > -1 && selectedNumbers.length < maxNumber)
if (selectedNumbers.length < maxNumber)
selectedNumbers.push(r);
else
console.log("array is full");
console.log(r, "["+selectedNumbers+"]");
};
for(let i = 0; i < 76; i++)
{
chooseNumber();
}
<button onclick="chooseNumber()">Generate</button>
Don't rely on a loop to generate a unique (unseen) integer in a limited range.
First, once all of the values in the range have been exhausted there will be no possibilities left, so you'll be left in an endless loop on the next invocation.
Second, it's wasteful of the processor because you are generating useless values on each invocation.
Instead, generate all of the values in range in advance (once), then shuffle them and get the last one from the array on each invocation (and throw an error when none remain):
/**
* Durstenfeld shuffle
*
* - https://stackoverflow.com/a/12646864/438273
* - https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
*/
function shuffleArray (array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
/** Get shuffled array of all integers in range */
function generateShuffledIntegerArray (min, max) {
const arr = [];
for (let i = min; i <= max; i += 1) arr.push(i);
shuffleArray(arr);
return arr;
}
const getUniqueInt = (() => {
const numbers = generateShuffledIntegerArray(1, 75);
return () => {
const n = numbers.pop();
if (typeof n === 'number') return n;
throw new Error('No unique numbers remaining');
};
})();
// Will log 75 integers, then throw on the 76th invocation:
for (let i = 1; i <= 76; i += 1) {
const n = getUniqueInt();
console.log(`${i}:`, n);
}
Code in TypeScript Playground
Your while loop is unnecessary. You could use an "if" statement instead.
To avoid clicking again on your button, you can do a recursive function like:
const chooseNumber = () => {
var r = Math.floor(Math.random() * 75) + 1;
console.log(r)
if(selectedNumbers.indexOf(r) === -1) {
selectedNumbers.push(r);
console.log(selectedNumbers);
} else {
chooseNumber();
}
};
let row = this.findEmpty(puzzleString)[0];
let col = this.findEmpty(puzzleString)[1];
let i = this.findEmpty(puzzleString)[2];
if(!this.findEmpty(puzzleString)) return puzzleString
for(let num = 1; num < 10; num++){
if(this.checkValue(puzzleString, row, col, num)){
puzzleString[i] = num;
this.solve(puzzleString)
}
}
findEmpty(puzzleString) iterates over the puzzle string and returns the row (A-I), column (1-9), and index of a blank grid.
checkValue() contains 3 helper functions returning a boolean ensuring there are no conflicts across row, column, or region.
The loop iterates from 1-9 and the first value from 1-9 that passes checkValue() is assigned to the current blank grid and then triggers recursion by calling the parent function solve().
What I don't understand is the next statement and how that triggers backtracking.
if(this.findEmpty(puzzleString)){
puzzleString[i] = '.';
}
If the current blank grid being checked has no solution then I think the grid remains a blank ('.'). If this is correct, why is this statement necessary? What about this statement is triggering backtracking?
My initial inclination is that this statement is a psuedo-else statement that runs only if the loop fails to find a solution. It has to be placed outside the loop in order to allow the full iteration of 1 through 9. But then how does the code know to run solve() afterwards if solve() is only called if checkValue() suceeds?
Here's the full code:
solve(puzzleString) {
let row = this.findEmpty(puzzleString)[0];
let col = this.findEmpty(puzzleString)[1];
let i = this.findEmpty(puzzleString)[2];
if(!this.findEmpty(puzzleString)) return puzzleString
for(let num = 1; num < 10; num++){
if(this.checkValue(puzzleString, row, col, num)){
puzzleString[i] = num;
this.solve(puzzleString)
}
}
if(this.findEmpty(puzzleString)){
puzzleString[i] = '.';
}
if(puzzleString.includes('.')) return { error: 'Puzzle cannot be solved' }
return {
solution: puzzleString.join('')
}
}
findEmpty(puzzleString){
for(let i = 0; i < puzzleString.length; i++){
if(puzzleString[i] == '.'){
let row = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
let col = (i % 9) + 1;
return [row, col, i];
}
}
return false;
}
checkValue(puzzleString, row, column, value){
if(this.checkRowPlacement(puzzleString, row, column, value)&&
this.checkColPlacement(puzzleString, row, column, value)&&
this.checkRegionPlacement(puzzleString, row, column, value)){
return true;
}
return false;
}
checkRowPlacement(puzzleString, row, column, value) {
let coordinates = [];
let rowLetter;
let colNum;
let temp = [];
if(row){row = row.toUpperCase();}
for(let i = 0; i < puzzleString.length; i++){
rowLetter = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
colNum = (i % 9) + 1;
coordinates.push(rowLetter + colNum);
}
for(let i = 0; i < coordinates.length; i++){
if(coordinates[i][0] == row){
temp.push(puzzleString[i]);
}
}
temp = temp.join('');
return !temp.includes(value) ? true : false
}
checkColPlacement(puzzleString, row, column, value) {
let coordinates = [];
let rowLetter;
let colNum;
let temp = [];
if(row){row = row.toUpperCase();}
for(let i = 0; i < puzzleString.length; i++){
rowLetter = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
colNum = (i % 9) + 1;
coordinates.push(rowLetter + colNum);
}
for(let i = 0; i < coordinates.length; i++){
if(coordinates[i][1] == column){
temp.push(puzzleString[i]);
}
}
temp = temp.join('');
return !temp.includes(value) ? true : false
}
checkRegionPlacement(puzzleString, row, column, value) {
let coordinates = [];
let rowLetter;
let colNum;
let regions = [];
if(row) row = row.toUpperCase();
for(let i = 0; i < puzzleString.length; i++){
rowLetter = String.fromCharCode('A'.charCodeAt(0) + Math.floor(i / 9));
colNum = (i % 9) + 1;
coordinates.push(rowLetter + colNum);
}
for(let i = 0; i < coordinates.length; i+=27){
for(let k = 0; k < 9; k+=3){
regions.push(
coordinates.slice(i+k,i+k+3) + ',' +
coordinates.slice(i+k+9, i+k+12) + ',' +
coordinates.slice(i+k+18, i+k+21)
)
}
}
let region = regions.filter(x => x.includes(row + column))[0].split(',').map(x => puzzleString[coordinates.indexOf(x)]).join('');
return region.includes(value) ? false : true;
}
The backtracking happens when findEmtpy returns false. But as your code is not optimal, still many other options are tried while backtracking: none of the for loops that are pending in the recursion tree are interrupted, yet it is wasted effort to have them continue and calling checkValue as each of those calls will now return false. So, eventually all those for loops will end, and the recursion will backtrack, only to finish yet another loop and backtrack again, ...etc.
Here is an update of your main function to avoid some of that overhead that leads to no gain:
solve(puzzleString) {
// Only call findEmpty once!
let emptyCell = this.findEmpty(puzzleString);
if (!emptyCell) return { solution: puzzleString.join('') }; // return the success object
let [row, col, i] = emptyCell; // use destructuring assignment
for (let num = 1; num < 10; num++) {
if (this.checkValue(puzzleString, row, col, num)) {
puzzleString[i] = num;
let result = this.solve(puzzleString); // capture the return value
if (result.solution) return result; // success: fast backtracking!
}
}
puzzleString[i] = "."; // could not solve this spot
// backtrack to possibly take a different route in previous decisions
return { error: 'Puzzle cannot be solved' };
}
I'm working on a card game with six available suits, but there can be 2-6 players and I only want the number of suits to match the amount of players.
let players = 4
const suits = [1, 2, 3, 4, 5, 6]
Of course, I want them to come out randomly.
I have come up with the following solution:
export function getRandomSuits(nrOfPlayers) {
const rangeMin = 1;
const rangeMax = 6;
const amount = nrOfPlayers;
let nums = [];
let getRandomNumber = function() {
return Math.floor((Math.random() * rangeMax) + rangeMin);
}
while (nums.length < amount) {
let nr = getRandomNumber();
var numberExists = _.indexOf(nums, nr);
console.log(numberExists);
if(numberExists < 0) {
nums.push(nr);
}
}
return nums;
}
It's been a while since I used the "while loop" thingy, so I'm not feeling very comfortable with it.
My questions are:
Is this good practice or is there a better solution?
Are there any performance or other practical reasons why this solution is bad?
Am I overthinking it?
In my opinion, I see no need of the function getRandomNumber(). Nonetheless, this is up to preference.
I would go with:
export function getRandomSuits(nrOfPlayers) {
const rangeMin = 1;
const rangeMax = 6;
let nums = [];
while (nums.length < nrOfPlayers) {
let nr = Math.floor((Math.random() * rangeMax) + rangeMin);
var numberExists = _.indexOf(nums, nr);
if(numberExists < 0) {
nums.push(nr);
}
}
return nums;
}
Not necessarily. Merely a matter of cleanliness and preference.
Maybe? :-)
Second method (Slightly better) with a temporary array:
export function getRandomSuits(nrOfPlayers) {
const rangeMin = 1;
const rangeMax = 6;
var tempNum = [];
for(i = 0; i <= rangeMax - rangeMin; i++){
tempNum[i] = rangeMin + i;
}
let nums = [];
while (nums.length < nrOfPlayers) {
let index = Math.floor(Math.random() * tempNum.length);
var numberExists = _.indexOf(nums, tempNum[index]);
if(numberExists < 0) {
nums.push(tempNum[index]);
tempNum.splice(tempNum, index));
}
}
return nums;
}
Hallo by lotto generator code I got some numbers double in one "generate" cycle. This is the code:
function lottery() {
for (var i=0; i<=7; i++) {
var lottery = Math.floor(Math.random() * 49);
document.getElementById ("lotto" + i).innerHTML = lottery;
}
}
Who knows where is the problem? Thank You!
Just for fun: O(N) solution (other posted are O(N^2))
It uses the Fisher-Yates shuffle algorithm and then after the first 7 elements have been shuffled it takes them as is.
So, no need to check if any of the numbers have already been drawn:
const arr = Array(50).fill(0).map((_, i) => i);
const DRAW = 7;
const rand = (from, to) => Math.floor(Math.random() * (to - from + 1)) + from;
for (let i = 0; i < DRAW; ++i) {
const r = rand(i, arr.length - 1);
[arr[r], arr[i]] = [arr[i], arr[r]];
}
const selected = arr.slice(0, DRAW);
References:
Online demo
Like #JNK commented, you need to store used values to avoid using them again.
The easiest way to do this would be to store them in an array, and check if they've been used.
function lottery() {
var used = [];
for (var i=0; i<=7; i++) {
var lottery;
while(true) { // this loop keeps going until a new number is found
lottery = Math.floor(Math.random() * 49);
var newNum = true;
for(var j=0; j<used.length; j++) {
if(used[j] == lottery) {
newNum = false; // if already used, set newNum to false
break;
}
}
if(newNum) { // if not already used, then add num to used array
used.push(lottery);
break;
}
}
document.getElementById ("lotto" + i).innerHTML = lottery;
}
}
All credits to JNK for this one from his link. This isn't very intuitive (it's minimized, like in code golf), but it's an interesting solution.
Optimized random number generator (86 bytes):
var getRandomLottoNumbers = function () {
var n=[],i=0;
for(;++i<50;)
n.push(i);
for(;--i>6;)
n.splice(i*Math.random()|0,1);
return n
};
Full explanation here.
Similar approach as the others, however just using one loop.
function lottery() {
var winners = [];
while (winners.length < 7) {
var lottery = Math.floor(Math.random() * 49);
if (winners.indexOf(lottery) === -1) {
winners.push(lottery);
document.getElementById ("lotto" + winners.length).innerHTML = lottery;
}
}
}
I'm trying to create a simple memory matching game and I'm having trouble assigning one number to each table cell from my cardValues array. My giveCellValue function is supposed to generate a random number then pick that number from the array and give it to one of the table cells but I'm a little over my head on this one and having trouble accomplishing this task.
var countCells;
var cardValues = [];
var checker = true;
var createTable = function (col, row) {
$('table').empty();
for (i = 1; i <= col; i++) {
$('table').append($('<tr>'));
}
for (j = 1; j <= row; j++) {
$('tr').append($('<td>'));
}
countCells = row * col;
};
createTable(3, 6);
for (i = 1; i <= countCells / 2; i++) {
cardValues.push(i);
if (i === countCells / 2 && checker) {
checker = false;
i = 0;
}
}
var giveCellValue = function () {
var random = Math.ceil(Math.random() * cardValues.length) - 1;
for (i = 0; i <= cardValues.length; i++) {
$('td').append(cardValues[random]);
cardValues.splice(random, 1);
}
};
giveCellValue();
console.log(cardValues);
Use
var countCells;
var cardValues = [];
var checker = true;
var createTable = function (col, row) {
$('table').empty();
for (var i = 0; i < col; i++) {
$('table').append($('<tr>'));
}
for (var j = 0; j < row; j++) {
$('tr').append($('<td>'));
}
countCells = row * col;
};
createTable(3, 6);
for (i = 0; i < countCells; i++) {
cardValues.push(i % 9 + 1);
}
var giveCellValue = function () {
var len = cardValues.length, tds = $('td');
for (var i = 0; i < len; i++) {
var random = Math.floor(Math.random() * cardValues.length);
tds.eq(i).append(cardValues.splice(random, 1));
}
};
giveCellValue();
console.log(cardValues);
Demo: Fiddle
Leaving aside the problems that have already been mentioned in the comments above...
If I understand it right, you want to assign each of the numbers in cardValues to a random table cell?
Seems like the problem is with this line:
var random = Math.ceil(Math.random() * cardValues.length) - 1;
What this does is generates a random number once. If you access the variable random at a later time, you're not calling the full line of code again, you're just getting the same value that was calculated the first time. E.g. if the above code runs, spits out '7' as its random number and stores that in random, then every time you go through the for loop, the value of random will always be '7' rather than being re-generated every time - make sense?
Try putting the randomiser inside the for loop - that way it will be run multiple times, generating a new random number every time:
var giveCellValue = function () {
var random;
for (i = 0; i <= cardValues.length; i++) {
random = Math.ceil(Math.random() * cardValues.length) - 1;
$('td').append(cardValues[random]);
cardValues.splice(random, 1);
}
};
Actually, I'd change line 4 above to random = Math.floor(Math.random() * cardValues.length); too which should have the same effect.