import { createApp } from "vue"; const utils = { updateIcons() { lucide.createIcons({ nameAttr: "icon", attrs: { width: "1.1em", height: "1.1em", }, }); }, /* eslint-disable no-bitwise */ randomSeed() { return (Math.random() * 2 ** 32) >>> 0; }, splitmix32(seed) { let localSeed = seed; // eslint-disable-next-line func-names return function () { localSeed |= 0; localSeed = (localSeed + 0x9e3779b9) | 0; let tmp = localSeed ^ (localSeed >>> 16); tmp = Math.imul(tmp, 0x21f0aaad); tmp ^= tmp >>> 15; tmp = Math.imul(tmp, 0x735a2d97); return ((tmp ^ (tmp >>> 15)) >>> 0) / 4294967296; }; }, /* eslint-enable no-bitwise */ randomInt(max, seed) { const prng = utils.splitmix32(seed); return Math.floor(prng() * max); }, shuffleSeeded(array, seed) { const output = array.slice(); const prng = utils.splitmix32(seed); for (let iteration = 0; iteration < array.length * 4; iteration += 1) { const i1 = Math.floor(prng() * array.length); const i2 = Math.floor(prng() * array.length); const tmp = output[i2]; output[i2] = output[i1]; output[i1] = tmp; } return output; }, setCookie(cname, cvalue, exdays) { const date = new Date(); date.setTime(date.getTime() + exdays * 24 * 60 * 60 * 1000); const expires = `expires=${date.toUTCString()}`; document.cookie = `${cname}=${cvalue}; path=/; ${expires}`; }, getCookie(cname, defaultValue) { const name = `${cname}=`; const decodedCookie = decodeURIComponent(document.cookie); const ca = decodedCookie.split(";"); for (let index = 0; index < ca.length; index += 1) { let cookie = ca[index]; while (cookie.charAt(0) === " ") { cookie = cookie.substring(1); } if (cookie.indexOf(name) === 0) { return cookie.substring(name.length, cookie.length); } } return defaultValue; }, gcd(numA, numB) { let tmpA = numA; let tmpB = numB; while (tmpA !== tmpB) { if (tmpA > tmpB) { tmpA -= tmpB; } else { tmpB -= tmpA; } } return tmpA; }, }; class TableGenerator { constructor(candidates, mixThreshold, slots, seed) { this.candidates = candidates; this.size = this.candidates.length; this.mixThreshold = mixThreshold; this.slots = slots; this.prng = utils.splitmix32(seed); this.indexScores = this.initIndexScores(); this.minIndexScore = 0; this.maxIndexScore = 0; this.mixScores = this.initMixScores(); this.minMixScore = 0; this.maxMixScore = 0; this.lastIndexes = []; this.mixTable = this.initMixTable(seed); } initIndexScores() { return Object.fromEntries(this.candidates.map((line, index) => [index, 0])); } initMixScores() { const scores = {}; for (let index1 = 0; index1 < this.candidates.length - 1; index1 += 1) { for ( let index2 = index1 + 1; index2 < this.candidates.length; index2 += 1 ) { scores[this.mixKey(index1, index2)] = 0; } } return scores; } initMixTable(seed) { const mixSlots = Math.round(this.mixThreshold * this.slots.length); if (mixSlots <= 0) { return [false]; } const gcd = utils.gcd(mixSlots, this.slots.length); const mixTableLength = this.slots.length / gcd; const tmpMixTable = new Array(mixTableLength) .fill(0) .map((unused, index) => index < mixSlots / gcd); return utils.shuffleSeeded(tmpMixTable, seed + 1); } // eslint-disable-next-line class-methods-use-this mixKey(index1, index2) { return Math.min(index1, index2) * 1000 + Math.max(index1, index2); } getRandomIndex() { return Math.floor(this.prng() * this.size); } getRandomMix() { const index1 = this.getRandomIndex(); let index2; do { index2 = this.getRandomIndex(); } while (index1 === index2); return [index1, index2]; } updateIndexScores(index1, index2) { for (let index = 0; index < this.size; index += 1) { if (index !== index1 && index !== index2) { this.indexScores[index] -= 1; } else { this.indexScores[index] = 0; } } this.minIndexScore = Math.min(...Object.values(this.indexScores)); this.maxIndexScore = Math.max(...Object.values(this.indexScores)); this.lastIndexes = [index1, index2]; } updateMixScores(index1, index2) { this.updateIndexScores(index1, index2); this.mixScores[this.mixKey(index1, index2)] += 1; this.minMixScore = Math.min(...Object.values(this.mixScores)); this.maxMixScore = Math.max(...Object.values(this.mixScores)); } indexScoreThreshold(value) { return ( this.minIndexScore + (this.maxIndexScore - this.minIndexScore) * value ); } mixScoreThreshold(value) { return this.minMixScore + (this.maxMixScore - this.minMixScore) * value; } getMixValue() { const indexScoreThreshold = this.indexScoreThreshold(this.mixThreshold); const mixScoreThreshold = this.mixScoreThreshold(0.1); let retries = 500; let index1, index2; do { [index1, index2] = this.getRandomMix(); } while ( (this.lastIndexes.includes(index1) || this.lastIndexes.includes(index2) || this.indexScores[index1] > indexScoreThreshold || this.indexScores[index2] > indexScoreThreshold || this.mixScores[this.mixKey(index1, index2)] > mixScoreThreshold) && (retries -= 1) > 0 ); if (retries === 0) { return null; } this.updateMixScores(index1, index2); return `${this.candidates[index1]} & ${this.candidates[index2]}`; } getCandidateValue() { const indexScoreThreshold = this.indexScoreThreshold(0.1); let retries = 500; let index; do { index = this.getRandomIndex(); } while ( (this.lastIndexes.includes(index) || this.indexScores[index] > indexScoreThreshold) && (retries -= 1) > 0 ); this.updateIndexScores(index); return this.candidates[index]; } getSlotValue(index) { if (this.mixTable[index % this.mixTable.length]) { const value = this.getMixValue(); if (value !== null) { return value; } } return this.getCandidateValue(); } generate() { return this.slots.map((slot, index) => [slot, this.getSlotValue(index)]); } } const VEGETABLES = { "πŸ₯¦": "Broccoli", "πŸ₯•": "Carrot", "πŸ§…": "Onion", "🌢️": "Pepper", "πŸ†": "Eggplant", "πŸ₯”": "Potato", "πŸ„": "Mushroom", "πŸ§„": "Garlic", "πŸ₯¬": "Lettuce", "πŸ₯’": "Cucumber", "πŸ₯‘": "Avocado", "🌽": "Corn", "🫘": "Beans", "🫚": "Ginger", "πŸ«›": "Pea", "🫜": "Radish", }; const app = createApp({ data() { return { config: { startTime: "21:00", endTime: "03:00", duration: 30, seed: utils.randomSeed(), candidates: "", endWithAll: true, mix: 25, }, table: [], copyTableOverride: null, }; }, computed: { vegetable() { return Object.keys(VEGETABLES)[ utils.randomInt(Object.keys(VEGETABLES).length, this.config.seed) ]; }, vegetable2() { return Object.keys(VEGETABLES)[ utils.randomInt(Object.keys(VEGETABLES).length, this.config.seed + 1) ]; }, startTimeMinute() { return Math.floor( Date.parse(`1970-01-01T${this.config.startTime}:00Z`) / 60000 ); }, endTimeMinute() { const result = Math.floor( Date.parse(`1970-01-01T${this.config.endTime}:00Z`) / 60000 ); return result < this.startTimeMinute ? result + 1440 : result; }, totalDuration() { return this.endTimeMinute - this.startTimeMinute; }, candidates() { return this.config.candidates .split("\n") .map((line) => line.trim()) .filter( (value, index, array) => value.length && array.indexOf(value) === index ); }, slotTooBig() { const slotCount = Math.ceil(this.totalDuration / this.config.duration); if (this.config.endWithAll) { return slotCount - 1 < this.candidates.length; } return slotCount < this.candidates.length; }, }, watch: { vegetable() { document.title = `${this.vegetable} LΓ©gume`; }, config: { handler() { this.saveConfig(); this.generateData(); }, deep: true, }, }, updated() { utils.updateIcons(); }, mounted() { document.title = `${this.vegetable} LΓ©gume`; setTimeout(this.showApp); utils.updateIcons(); this.newVegetables(); this.loadConfig(); this.generateData(); }, methods: { showApp() { document.getElementById("app").setAttribute("style", ""); }, getTime(minutes) { return `${Math.floor((minutes / 60) % 24) .toFixed(0) .padStart(2, "0")}:${(minutes % 60).toFixed(0).padStart(2, "0")}`; }, newVegetables() { this.config.candidates = utils .shuffleSeeded(Object.keys(VEGETABLES), utils.randomSeed()) .map((key) => `${key} ${VEGETABLES[key]}`) .slice(0, 6) .join("\n"); }, newSeed() { this.config.seed = utils.randomSeed(); }, saveConfig() { utils.setCookie("legume-config", JSON.stringify(this.config)); }, loadConfig() { const rawCookie = utils.getCookie("legume-config", null); if (rawCookie) { try { const parsedConfig = JSON.parse(rawCookie); Object.keys(parsedConfig).forEach((key) => { if (Object.hasOwn(this.config, key)) { this.config[key] = parsedConfig[key]; } }); } catch { /* Empty */ } } }, generateData() { this.table.splice(0, this.table.length); if (this.candidates.length <= 2) { return; } const duration = parseInt(this.config.duration, 10); const mixThreshold = parseInt(this.config.mix, 10) / 100; const slots = []; for ( let currentTimeMinute = this.startTimeMinute; currentTimeMinute < this.endTimeMinute; currentTimeMinute += duration ) { slots.push(this.getTime(currentTimeMinute)); } const generator = new TableGenerator( this.candidates, mixThreshold, slots, this.config.seed ); const table = generator.generate(); if (this.config.endWithAll) { table[table.length - 1][1] = "πŸ₯— SALAD πŸ₯—"; } this.table.push(...table); }, async copyTable() { const csvTable = this.table.map((row) => row.join("\t")).join("\n"); try { await navigator.clipboard.writeText(csvTable); this.copyTableOverride = "Table Copied"; } catch { this.copyTableOverride = "Error"; } setTimeout(() => { this.copyTableOverride = null; }, 1000); }, }, }); window.onload = () => { app.mount("#app"); };