Javascript Button Masher

in , , , ,
Estimated Reading Time: 6 minutes

A few years ago, I embarked on a journey to create a special game for my son, known as the Toddler Button Masher. Initially developed using Unity, it quickly became a source of endless amusement for him. When he hit a key, it changed the background and played a sound. Toddlers are easily amused, as are their parents.

Fast forward to today, where a low-end Chromebook usually finds its place in our living room. It’s not a very powerful device, and is not built for gaming engines. With this in mind, I took on the challenge of rewriting the game in JavaScript, ensuring that it runs seamlessly on this humble machine so that my youngest can enjoy what his older brother did.

Build Process

To expedite the development process, I decided to leverage the capabilities of ChatGPT, an advanced AI language model, to assist me in building the game’s code. I harnessed its coding abilities to bring the Toddler Button Masher game to life in a fraction of the time it would have taken me alone. The application was simple enough that it was unlikely to get distracted and the functionality is very straightforward. I also used the tool to discuss various frameworks such as Phaser, but decided they added too much overhead for such a simple game. I used the sounds from the previous version, and used prompts to generate some of the javascript.

This is an example of the code I was able to generate. There is nothing groundbreaking in the actual output, I would have been able to write any of this. However, I didn’t need to, and that’s very interesting.

async function loadSounds() {
  const soundPath = 'sounds/'; // Path to the sounds folder

  // Request permission to access the file system
  const fs = await window.showDirectoryPicker();

  // Get the directory handle for the sounds folder
  const soundsDirHandle = await fs.getDirectoryHandle(soundPath);

  // Create an array to store the loaded sound objects
  const loadedSounds = [];

  // Iterate over the files in the sounds folder and load each sound file dynamically
  for await (const fileHandle of soundsDirHandle.values()) {
    const file = await fileHandle.getFile();
    const sound = new Audio(URL.createObjectURL(file));
    loadedSounds.push(sound);
  }

  return loadedSounds;
}

The Game

  • Players can press any key on the keyboard or click the mouse to interact with the game.
  • Each keypress or mouse click triggers a visual effect and a corresponding sound effect.
  • The game provides a vibrant and dynamic visual experience with colorful animations.
  • It offers a playful and engaging way for children to explore the alphabet.
  • The game is designed to be intuitive and easy to play, suitable for very young players.
  • The gameplay encourages curiosity, creativity, and endless fun for toddlers.

Sound Conversion

The sounds I had from the Unity version were actually in WAV format. WAV is an old sound format developed by Microsoft and IBM. It is very good at functioning with incomplete data, which makes it useful for streaming. But WAV files are typically uncompressed and were way bigger than MP3s, a big part of MP3s’ initial popularity. I decided to use ChatGPT to write a bash script to help me convert the files to a compressed format. Again, a simple and well-defined task like a bash script turns out fairly well, it didn’t get a chance to hallucinate anything strange.

What I found interesting about this output is that it used ffmpeg. Other conversion options are available but this was quick and reliable. I used lame encoder years ago and the setup wasn’t very fun. I already had ffmpeg installed, so this was a very quick and easy solution.

#!/bin/bash

# Check if ffmpeg is installed
if ! command -v ffmpeg &> /dev/null; then
    echo "ffmpeg is not installed. Please install ffmpeg and try again."
    exit 1
fi

# Check if the folder path is provided as an argument
if [ $# -eq 0 ]; then
    echo "Please provide the folder path as an argument."
    exit 1
fi

# Store the folder path provided as an argument
folder_path=$1

# Check if the folder exists
if [ ! -d "$folder_path" ]; then
    echo "Folder '$folder_path' does not exist."
    exit 1
fi

# Convert each WAV file in the folder to MP3
for file in "$folder_path"/*.wav; do
    # Get the base name of the file
    filename=$(basename "$file")

    # Remove the extension to get the file name without extension
    filename_without_extension="${filename%.*}"

    # Construct the output file path with the MP3 extension
    output_file="$folder_path/$filename_without_extension.mp3"

    # Convert the WAV file to MP3 using ffmpeg
    ffmpeg -i "$file" "$output_file"
done

echo "Conversion complete."

The Code

The code is available on GitHub. Feel free to download or fork the project. It is just an html page, and doesn’t require a server or npm to run it. There isn’t much to it.

// Define your sound files
const soundFiles = [];
for (let i = 1; i <= 30; i++) {
  const filename = `game_over_${String(i).padStart(2, '0')}.mp3`;
  soundFiles.push(filename);
}

// Generate a random bright color
function generateRandomBrightColor() {
	const saturation = 100; // Maximum saturation for vibrant colors
	const lightness = 50; // Medium lightness for balanced colors
	const color = `hsl(${Math.random() * 360}, ${saturation}%, ${lightness}%)`;
	return color;
}

// Create a class for your application
class KeyPressApp {
	constructor() {
		this.displayElement = document.getElementById('display');
		this.audioElements = soundFiles.map(soundFile => this.createAudioElement(soundFile));
		this.currentColor = '';

		this.changeBackground();
		this.setupEventListeners();
	}

	createAudioElement(soundFile) {
		const audioElement = document.createElement('audio');
		audioElement.src = 'sounds/' + soundFile;
		document.body.appendChild(audioElement);
		return audioElement;
	}

	getRandomElementFromArray(array) {
		const randomIndex = Math.floor(Math.random() * array.length);
		return array[randomIndex];
	}

	changeBackground() {
		const randomColor = generateRandomBrightColor();
		document.body.style.backgroundColor = randomColor;
		this.currentColor = randomColor;
	}

	displayKeyPressed(key) {
		const contrastColor = this.getContrastColor(this.currentColor);
		this.displayElement.style.color = contrastColor;
		this.displayElement.innerText = this.getDisplayText(key).toUpperCase();
	}

	getContrastColor() {
		// Get the brightness value of the current color
		const brightness = this.getBrightness(this.currentColor);

		// Determine contrast color based on brightness
		const isLightColor = brightness > 0.5; // Adjust the threshold as desired

		return isLightColor ? '#000000' : '#ffffff';
	}

	getBrightness(color) {
		// Extract RGB values from the color
		const rgb = this.hexToRgb(color);
		const { r, g, b } = rgb;

		// Calculate the brightness based on RGB values
		const brightness = (r * 299 + g * 587 + b * 114) / 1000;

		// Normalize the brightness value between 0 and 1
		const normalizedBrightness = brightness / 255;

		return normalizedBrightness;
	}

	hexToRgb(hex) {
		// Remove the leading '#' if present
		const hexWithoutHash = hex.replace('#', '');

		// Split the hex value into RGB components
		const r = parseInt(hexWithoutHash.substr(0, 2), 16);
		const g = parseInt(hexWithoutHash.substr(2, 2), 16);
		const b = parseInt(hexWithoutHash.substr(4, 2), 16);

		// Return an object with the RGB values
		return { r, g, b };
	}

	getDisplayText(key) {
		if (key === ' ') {
			return 'SPACE';
		} else {
			const displayText = key.length === 1 ? key : key.toUpperCase();
			return displayText;
		}
	}

	handleKeyPress(event) {
		const key = event.key;
		const audioElement = this.getRandomElementFromArray(this.audioElements);

		this.displayKeyPressed(key);
		audioElement.play();
		this.changeBackground();
	}

	handleMouseClick(event) {
		const audioElement = this.getRandomElementFromArray(this.audioElements);

		this.displayKeyPressed('CLICK');
		audioElement.play();
		this.changeBackground();
	}

	handleMouseScroll(event) {
		const audioElement = this.getRandomElementFromArray(this.audioElements);

		this.displayKeyPressed('SCROLL');
		audioElement.play();
		this.changeBackground();
	}

	setupEventListeners() {
		document.addEventListener('keydown', this.handleKeyPress.bind(this));
		document.addEventListener('click', this.handleMouseClick.bind(this));
		document.addEventListener('contextmenu', this.handleMouseClick.bind(this)); // For right-click
		document.addEventListener('wheel', this.handleMouseScroll.bind(this));
	}
}

// Create an instance of the KeyPressApp class
const app = new KeyPressApp();