Lately I’ve become low-key obsessed with The Lucky Numbers Game. You have a grid of numbers, ranging from 1 to 100, and “behind” one of those numbers is a coveted prize. The catch is the winning number is randomly assigned, and each guess costs as much as the number you’re selecting. So guessing 13 will cost you $13 (or chips, tokens, credits, what have you), and guessing 72 will cost 72.
The goal is to discover the winning lucky number in as few guesses/lowest cost as possible.
It’s a math puzzle? (sigh) “I’m in, you son of a bitch.”
There are three obvious approaches to take when confronting a lucky number grid:
- start at 1, and systematically work your way towards 100
- start at 100, and systematically work your way towards 1
- make 100 random guesses, hoping to get lucky and pick it earlier rather than later
So which approach helps us find the lucky number fastest? That’s where the JavaScript comes in. We can build a lo-fi version of the Lucky Number game fairly quickly, and test each approach thousands of times to figure out what the average number of guesses and costs for each approach is.
// gets a random number between min and max, inclusive. Source: Mozilla Developer Network
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}
// find lucky number, going from 1 to 100, in sequence
function runLowToHighTrial(){
let guessHistory = [];
let guessTokens = 0;
let results = {};
// randomly assign our lucky number
let luckyNumber = getRandomIntInclusive(1,100);
// make guesses from 1 to 100, until lucky number is found
for (let x = 1; x <= 100; x++) {
thisGuess = x;
guessTokens = guessTokens + thisGuess;
if (thisGuess == luckyNumber){
break;
} else {
guessHistory.push(thisGuess);
}
}
results.luckyNumber = luckyNumber;
results.guessHistory = guessHistory;
results.guessTokens = guessTokens;
return results;
}
// find lucky number, going from 100 to 1, in sequence
function runHighToLowTrial(){
let guessHistory = [];
let guessTokens = 0;
let results = {};
// randomly assign our lucky number
let luckyNumber = getRandomIntInclusive(1,100);
// make guesses from 100 to 1, until lucky number is found
for (let x = 100; x >= 1; x--) {
thisGuess = x;
guessTokens = guessTokens + thisGuess;
if (thisGuess == luckyNumber){
break;
} else {
guessHistory.push(thisGuess);
}
}
results.luckyNumber = luckyNumber;
results.guessHistory = guessHistory;
results.guessTokens = guessTokens;
return results;
}
// find lucky number, random guesses, remember/ignore previous guesses
function runRandomTrial(){
let guessHistory = [];
let guessTokens = 0;
let results = {};
// randomly assign our lucky number
let luckyNumber = getRandomIntInclusive(1,100);
// make one hundred random guesses, no duplicates
for (let x = 1; x <= 100; x++) {
do{
thisGuess = getRandomIntInclusive(1,100);
} while ( guessHistory.indexOf(thisGuess) > -1 );
guessTokens = guessTokens + thisGuess;
if (thisGuess == luckyNumber){
break;
} else {
guessHistory.push(thisGuess);
}
}
results.luckyNumber = luckyNumber;
results.guessHistory = guessHistory;
results.guessTokens = guessTokens;
return results;
}
// calculate the average number of guesses and costs for each trial/approach
function getAverages(arrTrials){
let totalGuesses = 0;
let totalTokens = 0;
let results = {};
arrTrials.forEach( function(trial){
totalGuesses = totalGuesses + trial.guessHistory.length;
totalTokens = totalTokens + trial.guessTokens;
});
results.avgGuesses = totalGuesses / arrTrials.length;
results.avgTokens = totalTokens / arrTrials.length;
return results;
}
// we need to store our trial results
let lowToHighTrials = [];
let highToLowTrials = [];
let randomTrials = [];
// run 100K trials of each kind, storing results for averaging later
for (let a = 1; a <= 100000; a++){
lowToHighTrials.push( runLowToHighTrial() );
highToLowTrials.push( runHighToLowTrial() );
randomTrials.push( runRandomTrial() );
}
// calculate averages, show results
console.log("Low To High Trials:");
console.log( getAverages(lowToHighTrials) );
console.log("High To Low Trials:");
console.log( getAverages(highToLowTrials) );
console.log("Random Trials:");
console.log( getAverages(randomTrials) );
I decided to run 100,000 trials of each approach, and this is what I found:
Low To High Trials: { average Guesses: 49.57338, average Cost: 1721.8413 }
High To Low Trials: { average Guesses: 49.41141, average Cost: 3379.74295 }
Random Trials: { average Guesses: 49.48288, average Cost: 2549.10839 }
Although no approach has an edge over the others in the fewest number of guesses, there’s definitely a difference in cost between them!
To be fair, we’re testing each approach in isolation– as if the Lucky Number game has only a single player. In reality, the Lucky Number game can have multiple simultaneous players, each competing to uncover the winning number before another player does. Players seem to start off with the Low To High approach initially, but change over to the Random Pick approach, possibly succumbing to the pressure of “What if one of my competitors finds the winning number in a higher range while I’m wasting my time down here in the lower ranges?”
It might be interesting to modify the JavaScript to have multiple players searching for the lucky number using the different approaches, to see how they fare against each other in direct competition. But not right now, I need to eat breakfast.