import { getInputs } from './inputs.js';
// Weighting these callsign prefixes has been a journey...
// Originally, I weighted them based on rough analysis found here:
// https://github.com/sc0tfree/morsewalker/issues/8#issuecomment-2585349244
// However, as Mike N4FFF pointed out, this doesn't feel right, nor does it match
// the actual distribution of callsign prefixes in our logs.
// So, Mike suggested a much better approach, documented here:
// https://github.com/sc0tfree/morsewalker/issues/29
const US_CALLSIGN_PREFIXES_WEIGHTED = [
// Large items
{ value: 'K', weight: 40 }, // 40%
{ value: 'W', weight: 25 }, // 25%
{ value: 'N', weight: 20 }, // 20%
// Smaller items
{ value: 'AA', weight: 2 }, // 2%
{ value: 'AB', weight: 2 }, // 2%
{ value: 'AC', weight: 2 }, // 2%
{ value: 'AD', weight: 1 }, // 1%
{ value: 'AE', weight: 1 }, // 1%
{ value: 'AF', weight: 1 }, // 1%
{ value: 'AG', weight: 1 }, // 1%
{ value: 'AH', weight: 1 }, // 1%
{ value: 'AI', weight: 1 }, // 1%
{ value: 'AJ', weight: 1 }, // 1%
{ value: 'AK', weight: 1 }, // 1%
{ value: 'AL', weight: 1 }, // 1%
];
const NON_US_CALLSIGN_PREFIXES = [
'9A',
'CT',
'DL',
'E',
'EA',
'EI',
'ES',
'EU',
'F',
'G',
'GM',
'GW',
'HA',
'HB',
'I',
'JA',
'LA',
'LU',
'LY',
'LZ',
'OE',
'OH',
'OK',
'OM',
'ON',
'OZ',
'PA',
'PY',
'S',
'SM',
'SP',
'SV',
'UA',
'UR',
'VE',
'VK',
'YO',
'YT',
];
const stateAbbreviations = [
'AL',
'AK',
'AZ',
'AR',
'CA',
'CO',
'CT',
'DE',
'FL',
'GA',
'HI',
'ID',
'IL',
'IN',
'IA',
'KS',
'KY',
'LA',
'ME',
'MD',
'MA',
'MI',
'MN',
'MS',
'MO',
'MT',
'NE',
'NV',
'NH',
'NJ',
'NM',
'NY',
'NC',
'ND',
'OH',
'OK',
'OR',
'PA',
'RI',
'SC',
'SD',
'TN',
'TX',
'UT',
'VT',
'VA',
'WA',
'WV',
'WI',
'WY',
];
const names = [
'Adam',
'Ahmed',
'Ali',
'Amanda',
'Amy',
'Ana',
'Andrew',
'Angela',
'Anna',
'Anthony',
'Aria',
'Ashley',
'Barbara',
'Benjamin',
'Brandon',
'Brian',
'Charles',
'Christopher',
'Cynthia',
'Daniel',
'David',
'Deborah',
'Dennis',
'Donna',
'Dorothy',
'Edward',
'Elena',
'Elizabeth',
'Emily',
'Eric',
'Fatima',
'Frank',
'George',
'Gregory',
'Heather',
'Henry',
'Hong',
'Jack',
'Jacob',
'James',
'Jason',
'Jeffrey',
'Jennifer',
'Jessica',
'John',
'Jonathan',
'Joseph',
'Joshua',
'Justin',
'Karen',
'Katherine',
'Kathleen',
'Kevin',
'Kimberly',
'Larry',
'Laura',
'Linda',
'Lisa',
'Maria',
'Margaret',
'Mark',
'Mary',
'Matthew',
'Melissa',
'Michael',
'Michelle',
'Mohammad',
'Nancy',
'Nicole',
'Nicholas',
'Noor',
'Patricia',
'Patrick',
'Paul',
'Peter',
'Rebecca',
'Richard',
'Robert',
'Ronald',
'Ryan',
'Sandra',
'Sarah',
'Scott',
'Shirley',
'Sofia',
'Stephanie',
'Stephen',
'Steven',
'Susan',
'Thomas',
'Timothy',
'Tyler',
'Wei',
'William',
'Yan',
];
/**
* Retrieves the current user's station configuration.
*
* This function pulls data from the `getInputs` module to retrieve user-specific
* parameters like callsign, speed (WPM), volume, sidetone frequency, and name.
* If no inputs are available, it returns `null`. It also sets default values
* for `player` and `qsb`.
*
* @returns {Object|null} The user's station configuration or null if inputs are unavailable.
*/
export function getYourStation() {
let inputs = getInputs();
if (inputs === null) return;
return {
callsign: inputs.yourCallsign,
wpm: inputs.yourSpeed,
volume: inputs.yourVolume,
frequency: inputs.yourSidetone,
name: inputs.yourName,
state: inputs.yourState,
player: null,
qsb: false,
};
}
/**
* Generates a random calling station configuration.
*
* Uses `getInputs` to pull user-defined constraints like speed, volume, and tone ranges.
* Determines if the station is US-based or international with a 40% likelihood for US stations
* (unless `usOnly` is true). The station's attributes, including callsign, name, state,
* serial number, and CWOPS number, are randomly generated within the specified constraints.
* Additionally, introduces optional QSB (fading) parameters like frequency and depth.
*
* @returns {Object|null} The calling station configuration or null if inputs are unavailable.
*/
export function getCallingStation() {
let inputs = getInputs();
if (inputs === null) return;
// determine if it's a US station
let isUS = inputs.usOnly ? true : Math.random() < 0.4;
return {
callsign: isUS
? getRandomUSCallsign(inputs.formats)
: getRandomNonUSCallsign(inputs.formats),
wpm:
Math.floor(Math.random() * (inputs.maxSpeed - inputs.minSpeed + 1)) +
inputs.minSpeed,
enableFarnsworth: inputs.enableFarnsworth,
farnsworthSpeed: inputs.farnsworthSpeed || null,
volume:
Math.random() * (inputs.maxVolume - inputs.minVolume) + inputs.minVolume,
frequency: Math.floor(
Math.random() * (inputs.maxTone - inputs.minTone) + inputs.minTone
),
name: randomElement(names),
state: isUS ? randomElement(stateAbbreviations) : '',
serialNumber: (Math.floor(Math.random() * 30) + 1)
.toString()
.padStart(2, '0'),
cwopsNumber: Math.floor(Math.random() * 4000) + 1,
player: null,
qsb: inputs.qsb ? Math.random() < inputs.qsbPercentage / 100 : false,
// QSB frequency range: 0.05 to 0.5
qsbFrequency: Math.random() * 0.45 + 0.05,
// QSB depth range: 0.6 to 1.0
qsbDepth: Math.random() * 0.4 + 0.6,
};
}
/**
* Generates a random US amateur radio callsign.
*
* Based on the provided format (e.g., '1x1', '2x3'), this function builds a valid US callsign
* by combining a prefix, a digit, and a random sequence of letters. Defaults to a '1x3' format
* if an unknown format is passed. Utilizes predefined US callsign prefixes.
*
* @param {string[]} formats - An array of valid callsign formats.
* @returns {string} A randomly generated US callsign.
*/
function getRandomUSCallsign(formats) {
const format = randomElement(formats);
const number = randomDigit();
// If it’s a 1× format (1x1, 1x2, 1x3), we only want one-letter prefixes
let possiblePrefixes;
if (format.startsWith('1x')) {
possiblePrefixes = US_CALLSIGN_PREFIXES_WEIGHTED.filter(
(item) => item.value.length === 1
);
} else {
possiblePrefixes = US_CALLSIGN_PREFIXES_WEIGHTED;
}
const prefix = weightedRandomElement(possiblePrefixes);
let prefixLettersToGenerate = parseInt(format.slice(0, 1)) - prefix.length;
switch (format) {
case '1x1':
return `${prefix}${number}${generateRandomLetters(1)}`;
case '1x2':
return `${prefix}${number}${generateRandomLetters(2)}`;
case '1x3':
return `${prefix}${number}${generateRandomLetters(3)}`;
case '2x1':
return `${prefix}${generateRandomLetters(prefixLettersToGenerate)}${number}${generateRandomLetters(1)}`;
case '2x2':
return `${prefix}${generateRandomLetters(prefixLettersToGenerate)}${number}${generateRandomLetters(2)}`;
case '2x3':
return `${prefix}${generateRandomLetters(prefixLettersToGenerate)}${number}${generateRandomLetters(3)}`;
default:
return `${prefix}${number}${generateRandomLetters(3)}`; // Default to '1x3'
}
}
/**
* Generates a random non-US amateur radio callsign.
*
* Combines a random international prefix with a digit and a sequence of letters
* according to the specified format. Ensures compatibility between prefix length
* and format requirements. Retries until a valid combination is found for prefixes
* and formats. Leverages predefined international prefixes.
*
* @param {string[]} formats - An array of valid callsign formats.
* @returns {string} A randomly generated non-US callsign.
*/
function getRandomNonUSCallsign(formats) {
let prefix, format;
do {
prefix = randomElement(NON_US_CALLSIGN_PREFIXES);
format = randomElement(formats);
} while (format.startsWith('1x') && prefix.length !== 1);
const number = randomDigit();
const lettersBeforeNumber = format.startsWith('2x') ? 2 - prefix.length : 0;
const lettersAfterNumber = parseInt(format.slice(-1));
return `${prefix}${generateRandomLetters(lettersBeforeNumber)}${number}${generateRandomLetters(lettersAfterNumber)}`;
}
/**
* Creates a random sequence of letters.
*
* Utilizes the English alphabet to generate a string of random uppercase letters
* with the specified length.
*
* @param {number} length - The number of letters to generate.
* @returns {string} A string of random uppercase letters.
*/
function generateRandomLetters(length) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Array.from({ length }, () => randomElement(alphabet)).join('');
}
/**
* Selects a random element from an array.
*
* Picks and returns one random element from the given array using a uniform distribution.
*
* @param {Array} array - The array to select a random element from.
* @returns {*} A random element from the array.
*/
function randomElement(array) {
return array[Math.floor(Math.random() * array.length)];
}
/**
* Selects a random element from an array using weighted values.
*
* Each object in the array should have:
* - value: the item you want to pick
* - weight: the numeric weight (or percentage) of that item
*
* Example usage:
* ```js
* const fruits = [
* { value: 'apple', weight: 50 }, // 50% chance
* { value: 'banana', weight: 30 }, // 30% chance
* { value: 'mango', weight: 20 }, // 20% chance
* ];
*
* const pickedFruit = weightedRandomElement(fruits);
* console.log(pickedFruit); // Logs 'apple', 'banana', or 'mango' based on weights
* ```
*
* @param {Array} weightedArray - The array of objects to select from.
* @returns {*} A random element's `value` from the array, based on the weights.
*/
function weightedRandomElement(weightedArray) {
// Sum all weights
const totalWeight = weightedArray.reduce((sum, item) => sum + item.weight, 0);
// Pick a random number between 0 and totalWeight
let randomValue = Math.random() * totalWeight;
// Determine which item is 'hit' by randomValue
for (const item of weightedArray) {
randomValue -= item.weight;
if (randomValue <= 0) {
return item.value;
}
}
// Fallback (should not happen if weights are set up correctly)
return null;
}
/**
* Generates a random single-digit number.
*
* Returns a random integer between 0 and 9 inclusive.
*
* @returns {number} A random single-digit number.
*/
function randomDigit() {
return Math.floor(Math.random() * 10);
}
// // Test cases for each callsign type individually
// const formats = ['1x1', '1x2', '1x3', '2x1', '2x2', '2x3'];
//
// console.log("---- US Callsigns (Individual Formats) ----");
// for (const format of formats) {
// console.log(`US Callsign with format '${format}':`);
// getRandomCallsign(true, [format]);
// }
//
// console.log("\n---- Non-US Callsigns (Individual Formats) ----");
// for (const format of formats) {
// console.log(`Non-US Callsign with format '${format}':`);
// getRandomCallsign(false, [format]);
// }
//
// // Test cases with multiple formats
// const multipleFormats = ['1x1', '2x2', '1x3'];
//
// console.log("\n---- US Callsign with Multiple Formats ----");
// for (let i = 0; i < 10; i++) {
// getRandomCallsign(true, formats);
// }
//
// // Test cases to demonstrate the 40% US and 60% Non-US chance when usOnly is false
// console.log("\n---- Random Callsigns with 40% US and 60% Non-US ----");
// for (let i = 0; i < 10; i++) {
// getRandomCallsign(false, formats);
// }