The Lucky Numbers Game and JS Simulations

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:

  1. start at 1, and systematically work your way towards 100
  2. start at 100, and systematically work your way towards 1
  3. 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.

Jack Has Problems With His Dates

A long time ago, Jack needed a function to return a JavaScript date object which represented New Year’s Day for any year which he specified, or for the current year if none was provided.

This is what he came up with:

function getNewYearsDate(yr){

	var nyDate = new Date("");

	if ( typeof yr === "undefined"){
		var tmpDate = new Date();
		yr = tmpDate.getFullYear();
	}

	nyDate.setMonth(0);
	nyDate.setDate(1);
	nyDate.setYear(yr);

	return nyDate;
}

Of course, Jack tested his function carefully . . .

getNewYearsDate(2020);
// returns Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)
getNewYearsDate();
// returns Tue Jan 01 2019 00:00:00 GMT-0500 (Eastern Standard Time)

and seeing the results he expected to see, Jack pushed this change to production where it worked wonderfully for many months.

This morning, Jack identified a need for a similar function which represents July 4th/Independence Day. Because Jack is in a rush, he decides to copy and alter his getNewYearsDate function to handle returning the date for Independence Day in the United States (July 4th).

Here’s what he did:

function getIndependenceDate(yr){
	
	var idDate = new Date("");

	if ( typeof yr === "undefined"){
		var tmpDate = new Date();
		yr = tmpDate.getFullYear();
	}

	idDate.setMonth(6);
	idDate.setDate(4);
	idDate.setYear(yr);

	return idDate;
}

But this time, when he tests his new function, he gets surprising results!

getIndependenceDate()
Tue Jan 01 2019 00:00:00 GMT-0500 (Eastern Standard Time)
getIndependenceDate(2020)
Wed Jan 01 2020 00:00:00 GMT-0500 (Eastern Standard Time)

Can you see why the getNewYearsDate() function works, while the getIndependenceDate() function fails?

If so, how would you alter the getIndependenceDate() function so it returns the correct value?

New Repo: CFC for CSV

As I mentioned before, I’ve been working with SiteImprove’s API recently. As part of my task, I had to stitch all the data retrieved together in a large file which could be opened with and manipulated by Excel, Google Sheets, etc. Using the cfspreadsheet tag/functions seemed like overkill, so I wrote something “quick & dirty” to convert an array of structures into a .csv file.

Although the solution I came up with worked, it was tied to the headers/columns of the SiteImprove report. This bothered me, so I came up with a CFML component which transforms queries into comma-separated-value output for a more generic, reusable approach.