morse-game/morse-code_files/morse-code.js
Nathan McRae 7c9d161719 Add common words back in as option
Still can choose the morse shorthand
2024-05-08 20:16:39 -07:00

692 lines
21 KiB
JavaScript

const FREQUENCY = 440;
var DOT_TIME = 300;
var DASH_TIME = DOT_TIME * 3;
var SYMBOL_BREAK = DOT_TIME;
var LETTER_BREAK = DOT_TIME * 3;
var WORD_BREAK = DOT_TIME * 7;
let note_context;
let note_node;
let gain_node;
let audioContextInitialized = false;
async function initializeAudioContext() {
note_context = new AudioContext();
await note_context.resume();
note_node = note_context.createOscillator();
gain_node = note_context.createGain();
note_node.frequency.value = FREQUENCY.toFixed(2);
gain_node.gain.value = 0;
note_node.connect(gain_node);
gain_node.connect(note_context.destination);
note_node.start();
audioContextInitialized = true;
}
function startNotePlaying() {
// Pass a start time of 0 so it starts ramping up immediately.
gain_node.gain.setTargetAtTime(0.1, 0, 0.001)
$("#beepLamp").addClass("lamp-on")
}
function stopNotePlaying() {
// Pass a start time of 0 so it starts ramping down immediately.
gain_node.gain.setTargetAtTime(0, 0, 0.001)
$("#beepLamp").removeClass("lamp-on")
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Global strictly increasing play counter to avoid multiple replays at the same time.
var playCounter = 0;
async function playDash(currentPlayCounter) {
if (currentPlayCounter != playCounter) { return; }
startNotePlaying();
await sleep(DASH_TIME);
stopNotePlaying();
}
async function playDot(currentPlayCounter) {
if (currentPlayCounter != playCounter) { return; }
startNotePlaying();
await sleep(DOT_TIME);
stopNotePlaying();
}
/**
* message is something like '---'
*/
async function playLetter(letter, currentPlayCounter) {
// console.log('playSentence with', playCounter, currentPlayCounter);
if (!audioContextInitialized) {
initializeAudioContext();
}
// console.log('playing', letter);
for (let i = 0; i < letter.length; i++) {
if (currentPlayCounter != playCounter) { return; }
if (letter[i] == '-') {
await playDash(currentPlayCounter);
} else if (letter[i] == '.') {
await playDot(currentPlayCounter);
}
await sleep(SYMBOL_BREAK);
}
}
// Word is an array of letters, like ['.', '.-', '-']
async function playWord(word, currentPlayCounter) {
// console.log('playSentence with', playCounter, currentPlayCounter);
for (let i = 0; i < word.length; i++) {
if (currentPlayCounter != playCounter) { return; }
await playLetter(word[i], currentPlayCounter);
await sleep(LETTER_BREAK);
}
}
// Sentence is an array of words.
// Ex. "dog is good" -> [['-..', '---', '--.'], ['..', '...'], ['--.', '---', '---', '-..']]
async function playSentence(sentence, currentPlayCounter) {
// console.log('playSentence with', playCounter, currentPlayCounter);
if (currentPlayCounter != playCounter) { return; }
// Slight pause before it starts
// console.log('playSentence with', playCounter, currentPlayCounter);
await sleep(LETTER_BREAK);
// console.log('playSentence with', playCounter, currentPlayCounter);
for (let i = 0; i < sentence.length; i++) {
if (currentPlayCounter != playCounter) { return; }
await playWord(sentence[i], currentPlayCounter);
await sleep(WORD_BREAK);
}
}
// asciiChar is something like 'd'
// Assumes [a-z0-9]
function convertAsciiCharToMorse(asciiChar) {
return MORSE_MAP[asciiChar];
}
// asciiWord is something like 'dog'
// Assumes [a-z0-9]
function convertAsciiWordToMorse(asciiWord) {
return asciiWord.split('').map(convertAsciiCharToMorse);
}
// asciiSentence is something like 'dog is good'
// Assumes words are separated by spaces.
function convertAsciiSentenceToMorse(asciiSentence) {
let splitSentence = asciiSentence.toUpperCase().split(' ');
return splitSentence.map(convertAsciiWordToMorse);
}
function getRandomLetter() {
const randomIndex = Math.floor(Math.random() * 26);
return Object.keys(MORSE_MAP)[randomIndex];
}
function getRandomWord() {
const randomIndex = Math.floor(Math.random() * EASY_WORDS.length);
return EASY_WORDS[randomIndex];
}
function getRandomMorseWord() {
const randomIndex = Math.floor(Math.random() * MORSE_WORDS.length);
return MORSE_WORDS[randomIndex];
}
function getRandomEasyWords() {
const totalLength = Math.floor(Math.random() * 3) + 2;
let finalSentence = ''
for (let i = 0; i < totalLength; i++) {
finalSentence += EASY_WORDS[Math.floor(Math.random() * EASY_WORDS.length)];
if (i < totalLength - 1) {
finalSentence += ' ';
}
}
return finalSentence;
}
function getRandomMorseWords() {
const totalLength = Math.floor(Math.random() * 4) + 2;
let finalSentence = ''
for (let i = 0; i < totalLength; i++) {
finalSentence += MORSE_WORDS[Math.floor(Math.random() * MORSE_WORDS.length)];
if (i < totalLength - 1) {
finalSentence += ' ';
}
}
return finalSentence;
}
function updateSpeed() {
const useFarnsworth = document.querySelector('input[name="farnsworth"]:checked').value == 'yes';
// Get the difficulty and update the constants.
const difficulty = document.querySelector('input[name="speed"]:checked').value
if (useFarnsworth) {
DOT_TIME = 60;
DASH_TIME = DOT_TIME * 3;
SYMBOL_BREAK = DOT_TIME;
let letterBreakMultiplier;
switch (difficulty) {
case 'easy':
letterBreakMultiplier = 24;
break;
case 'medium':
letterBreakMultiplier = 12;
break;
case 'hard':
letterBreakMultiplier = 6;
break;
default:
letterBreakMultiplier = 3;
break;
}
LETTER_BREAK = DOT_TIME * letterBreakMultiplier;
WORD_BREAK = DOT_TIME * (letterBreakMultiplier * 2.5);
} else {
switch (difficulty) {
case 'easy':
DOT_TIME = 300;
break;
case 'medium':
DOT_TIME = 200;
break;
case 'hard':
DOT_TIME = 100;
break;
default:
DOT_TIME = 60;
break;
}
DASH_TIME = DOT_TIME * 3;
SYMBOL_BREAK = DOT_TIME;
LETTER_BREAK = DOT_TIME * 3;
WORD_BREAK = DOT_TIME * 7;
}
}
function getTarget() {
let target;
// Get the difficulty and assign a random target.
const difficulty = document.querySelector('input[name="difficulty"]:checked').value
switch (difficulty) {
case 'easy':
target = getRandomLetter();
break;
case 'medium':
target = getRandomWord();
break;
case 'hard':
target = getRandomEasyWords();
break;
case 'morse-easy':
target = getRandomMorseWord();
break;
case 'morse-hard':
target = getRandomMorseWords();
break;
default:
target = getRandomWords();
break;
}
return target;
}
var startButton = document.getElementById('playGame');
class ListeningGame {
constructor(wordInput, statusElement, submitButton, resetButton) {
this.stopped = false;
this.messageFound = false;
// DOM elements we'll interact with
this.wordInput = wordInput;
this.statusElement = statusElement;
this.submitButton = submitButton;
this.resetButton = resetButton;
// Bind listeners to `this`.
this.inputListener = this.inputListener.bind(this);
this.submit = this.submit.bind(this);
this.resetButtonListener = this.resetButtonListener.bind(this);
// Initialize listeners
this.wordInput.addEventListener('keyup', this.inputListener);
this.submitButton.addEventListener('click', this.submit);
this.resetButton.addEventListener('click', this.resetButtonListener);
this.wordInput.removeAttribute('disabled');
this.submitButton.removeAttribute('disabled');
this.resetButton.removeAttribute('disabled');
this.target = '';
}
startNewGame() {
// Start listening
if (!audioContextInitialized) {
initializeAudioContext();
}
// Focus on the word input box.
this.wordInput.focus();
// Update speed based on difficulty.
updateSpeed();
// Get a new target
this.target = getTarget();
// console.log('target: ', this.target);
// Reset
this.messageFound = false;
this.wordInput.value = '';
this.statusElement.textContent = 'Waiting...';
this.playTarget();
}
submit(event) {
event.preventDefault();
if (this.messageFound) {
// If we found it already, get a new target and play it.
this.startNewGame();
} else {
const enteredWord = this.wordInput.value;
// console.log('enteredWord: ', enteredWord, 'target: ', this.target);
if (enteredWord.toUpperCase() == this.target) {
// If they got the word, show message and switch event listener.
this.statusElement.textContent = 'Correct! Press enter to play a new round.';
this.messageFound = true;
// Extra playCounter increase to stop any current playing.
this.stopCurrentPlaying();
} else {
// If they didn't, clear the text box and let them try again.
this.statusElement.textContent = 'Not quite! Try again.';
this.replay();
}
}
}
stopCurrentPlaying() {
// console.log('stopping current playing');
// Also increase playCounter to stop any current playing. Wastes a count, but we have the bits to spare.
playCounter += 1;
}
async replay() {
if (this.messageFound) {
this.startNewGame();
return;
}
// Stop the current playing, and play the target again.
this.stopCurrentPlaying();
this.wordInput.focus();
this.playTarget();
}
resetButtonListener(event) {
event.preventDefault();
this.replay();
}
inputListener(event) {
if (this.messageFound) {
this.startNewGame();
return;
}
if (event.key == 'Control') {
this.replay();
} else if (event.key == 'Enter') {
this.submit(event);
}
}
playTarget() {
playCounter += 1;
// console.log('playing with counter', playCounter)
playSentence(convertAsciiSentenceToMorse(this.target), playCounter);
}
stopGame() {
this.stopCurrentPlaying();
this.wordInput.value = '';
this.statusElement.textContent = 'Press Start to begin.';
this.wordInput.removeEventListener('keyup', this.inputListener);
this.submitButton.removeEventListener('click', this.submit);
this.resetButton.removeEventListener('click', this.resetButtonListener);
this.wordInput.setAttribute('disabled', 'disabled');
this.submitButton.setAttribute('disabled', 'disabled');
this.resetButton.setAttribute('disabled', 'disabled');
}
}
var currentGame;
var playListeningGameButton = document.getElementById("playListeningGame");
async function playListeningGame() {
// Check if there's a currentGame already. If there is, stop it!
if (currentGame) {
currentGame.stopGame();
}
const wordInput = document.getElementById('wordInput');
const status = document.getElementById('status');
const submitButton = document.getElementById('submitButton');
const resetButton = document.getElementById('resetButton');
currentGame = new ListeningGame(wordInput, status, submitButton, resetButton);
currentGame.startNewGame();
playListeningGameButton.removeEventListener('click', playListeningGame);
playListeningGameButton.addEventListener('click', stopListeningGame);
playListeningGameButton.textContent = 'Stop';
}
async function stopListeningGame() {
if (currentGame) {
// Stop current game
currentGame.stopGame();
}
playListeningGameButton.removeEventListener('click', stopListeningGame);
playListeningGameButton.addEventListener('click', playListeningGame);
playListeningGameButton.textContent = 'Start';
}
playListeningGameButton.addEventListener('click', playListeningGame);
class InputGame {
constructor(signalButton, startOverButton, targetDisplay, inputDisplay, inputStatus) {
this.signalButton = signalButton;
this.startOverButton = startOverButton;
this.targetDisplay = targetDisplay;
this.inputDisplay = inputDisplay;
this.inputStatus = inputStatus;
this.target = '';
// Stack for keeping track of dots and dashes that make up letter
this.dotAndDashStack = [];
// Stack for keeping track of letters that make up word
this.letterStack = [];
this.lastKeyDownTime = 0;
this.lastKeyUpTime = 0;
this.letterTimeout = null;
this.spaceTimeout = null;
this.matchFound = false;
// Keep track of where the spacebar is to prevent duplicate keydowns when you hold it down.
this.spacebarDown = false;
// Keep track of if the tap key is down or not to prevent weird state issues when restarting game.
// this.keyIsDown = false;
this.documentKeydownListener = this.documentKeydownListener.bind(this);
this.documentKeyupListener = this.documentKeyupListener.bind(this);
this.buttonMousedownListener = this.buttonMousedownListener.bind(this);
this.buttonMouseupListener = this.buttonMouseupListener.bind(this);
this.startOver = this.startOver.bind(this);
this.handleLetterTimeout = this.handleLetterTimeout.bind(this);
this.handleSpaceTimeout = this.handleSpaceTimeout.bind(this);
document.addEventListener('keydown', this.documentKeydownListener);
document.addEventListener('keyup', this.documentKeyupListener);
this.signalButton.addEventListener('mousedown', this.buttonMousedownListener);
this.signalButton.addEventListener('mouseup', this.buttonMouseupListener);
this.signalButton.addEventListener('touchstart', this.buttonMousedownListener);
this.signalButton.addEventListener('touchend', this.buttonMouseupListener);
this.startOverButton.addEventListener('click', this.startOver);
}
startNewGame() {
this.matchFound = false;
this.startOver();
// Update speed based on difficulty.
updateSpeed();
// Get target word
this.target = getTarget();
// console.log('target: ', this.target);
// Update target display with the new target
this.targetDisplay.textContent = 'Target: ' + this.target;
this.inputDisplay.textContent = '';
this.inputStatus.textContent = '';
this.signalButton.removeAttribute('disabled');
this.startOverButton.removeAttribute('disabled');
// Focus on the tap button
this.signalButton.focus();
// Start listening
if (!audioContextInitialized) {
initializeAudioContext();
}
}
keyDown() {
if (this.matchFound) {
// console.log('match found! doing nothing on keydown')
return;
}
startNotePlaying();
const currentTime = Date.now();
// Update the marker for the last signal ending
this.lastKeyDownTime = currentTime;
// Get the total silence time since we last had a signal
const silenceDelta = currentTime - this.lastKeyUpTime;
this.handleSilence(silenceDelta);
// We also want to clear any pending intervals so we don't duplicate them.
clearTimeout(this.letterTimeout);
clearTimeout(this.spaceTimeout);
}
keyUp() {
if (this.matchFound) {
this.startNewGame();
return;
}
// console.log('keyup!');
stopNotePlaying();
const currentTime = Date.now();
// Update the marker for the last silence ending
this.lastKeyUpTime = currentTime;
// Get the total signal time since we last had silence
const signalDelta = currentTime - this.lastKeyDownTime;
this.handleSignal(signalDelta);
// TODO: start timer
this.letterTimeout = setTimeout(this.handleLetterTimeout, LETTER_BREAK);
this.spaceTimeout = setTimeout(this.handleSpaceTimeout, WORD_BREAK);
}
handleLetterTimeout() {
this.handleSilence(LETTER_BREAK);
}
handleSpaceTimeout() {
this.handleSilence(WORD_BREAK)
}
handleSignal(signalDelta) {
// console.log('signalDelta: ', signalDelta)
// See if it's a dot or a dash
if (signalDelta < DASH_TIME) {
this.dotAndDashStack.push('.');
} else {
this.dotAndDashStack.push('-');
}
}
handleSilence(silenceDelta) {
// If there's no input yet, don't do anything
if (this.dotAndDashStack.length == 0 && this.letterStack.length == 0) {
return;
}
// console.log('silenceDelta: ', silenceDelta, 'LETTER_BREAK: ', LETTER_BREAK);
// If we have at least a letter break, try to add a new character if we have any dots or dashes
if (silenceDelta >= LETTER_BREAK && this.dotAndDashStack.length != 0) {
// TODO: handle letter being done.
const currentLetterCombo = this.dotAndDashStack.join('');
if (currentLetterCombo in REVERSE_MORSE_MAP) {
this.letterStack.push(REVERSE_MORSE_MAP[currentLetterCombo]);
} else {
// Push a question mark
this.letterStack.push('?');
}
// Clear current stack.
this.dotAndDashStack = [];
}
// If we have more than a word break, we may also want to add a space after doing the above.
// We may want to do this without dots and dashes (e.g. the timeout), but only if the last char
// isn't a space
if (silenceDelta >= WORD_BREAK &&
this.letterStack.length != 0 &&
this.letterStack[this.letterStack.length - 1] != ' ') {
this.letterStack.push(' ');
}
// Update the display.
this.inputDisplay.textContent = this.letterStack.join('');
this.checkForMatch();
}
documentKeydownListener(event) {
event.preventDefault();
// console.log('document keydown');
if (event.key == ' ' && !this.spacebarDown) {
// console.log('sending keydown');
this.spacebarDown = true;
this.keyDown();
} else if (event.key == 'Control') {
this.startOver();
}
}
documentKeyupListener(event) {
event.preventDefault();
if (this.matchFound) {
this.spacebarDown = false;
this.startNewGame();
return;
}
// console.log('document keyup');
// console.log(this);
if (event.key == ' ') {
this.spacebarDown = false;
this.keyUp();
}
}
buttonMousedownListener(event) {
event.preventDefault();
// console.log('button mousedown');
// console.log(this);
this.keyDown();
}
buttonMouseupListener(event) {
event.preventDefault();
// console.log('button mouseup');
// console.log(this);
this.keyUp();
}
startOver(event) {
if (event) {
event.preventDefault();
}
if (this.matchFound) {
this.startNewGame();
return;
}
this.inputDisplay.textContent = '';
this.inputStatus.textContent = '';
this.dotAndDashStack = [];
this.letterStack = [];
this.lastKeyDownTime = 0;
this.lastKeyUpTime = 0;
clearTimeout(this.letterTimeout);
clearTimeout(this.spaceTimeout);
}
checkForMatch() {
// console.log('checking for match');
// console.log('target: \'' + this.target + '\'');
// console.log('this.letterStack.join(\'\'): \'' + this.letterStack.join('') + '\'');
// console.log(this.letterStack.join('') == this.target);
if (this.letterStack.join('') == this.target) {
// console.log('match!');
// Clear intervals
clearTimeout(this.letterTimeout);
clearTimeout(this.spaceTimeout);
// Show message
this.inputStatus.textContent = 'You got it! Press any key or click either button to start a new round.';
this.matchFound = true;
}
}
stopGame() {
this.startOver();
this.targetDisplay.textContent = 'Press Start to begin.';
this.inputStatus.textContent = '';
document.removeEventListener('keydown', this.documentKeydownListener);
document.removeEventListener('keyup', this.documentKeyupListener);
this.signalButton.removeEventListener('mousedown', this.buttonMousedownListener);
this.signalButton.removeEventListener('mouseup', this.buttonMouseupListener);
this.signalButton.removeEventListener('touchstart', this.buttonMousedownListener);
this.signalButton.removeEventListener('touchend', this.buttonMouseupListener);
this.startOverButton.removeEventListener('click', this.startOver);
this.signalButton.setAttribute('disabled', 'disabled');
this.startOverButton.setAttribute('disabled', 'disabled');
}
}
var playInputGameButton = document.getElementById("playInputGame");
async function playInputGame() {
// TODO: check if there's a currentGame already. If there is, stop it!
if (currentGame) {
currentGame.stopGame();
}
const signalButton = document.getElementById('signalButton');
const startOverButton = document.getElementById('startOverButton');
const targetDisplay = document.getElementById('targetDisplay');
const inputDisplay = document.getElementById('inputDisplay');
const inputStatus = document.getElementById('inputStatus');
currentGame = new InputGame(signalButton, startOverButton, targetDisplay, inputDisplay, inputStatus);
currentGame.startNewGame();
playInputGameButton.removeEventListener('click', playInputGame);
playInputGameButton.addEventListener('click', stopInputGame);
playInputGameButton.textContent = 'Stop';
}
async function stopInputGame() {
if (currentGame) {
// Stop current game
currentGame.stopGame();
}
playInputGameButton.removeEventListener('click', stopInputGame);
playInputGameButton.addEventListener('click', playInputGame);
playInputGameButton.textContent = 'Start';
}
playInputGameButton.addEventListener('click', playInputGame);
// For iOS, you need a touch event before you can play audio!
var initializeAudioOnTouch = function() {
if (!audioContextInitialized) {
initializeAudioContext();
}
document.removeEventListener('touchend', initializeAudioOnTouch);
}
document.addEventListener('touchend', initializeAudioOnTouch);