v2
This commit is contained in:
+80
-17
@@ -1,33 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, onMounted, computed, watch } from "vue";
|
||||
import LucideIcon from "./components/LucideIcon.vue";
|
||||
import CustomButton from "./components/CustomButton.vue";
|
||||
import { DEFAULT_CONFIG, VEGETABLES } from "./contants";
|
||||
import { type Config } from "./interfaces";
|
||||
import { randomElement } from "./lib/random";
|
||||
import { TableGenerator } from "./lib/table-gen";
|
||||
import { formatTime, timeToMinute } from "./lib/time";
|
||||
import ConfigTable from "./components/ConfigTable.vue";
|
||||
import OutputTable from "./components/OutputTable.vue";
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const config = ref<Config>(DEFAULT_CONFIG);
|
||||
const table = ref<[string, string][]>([]);
|
||||
|
||||
const vegetable = computed<string>(() =>
|
||||
randomElement(Object.keys(VEGETABLES), config.value.seed),
|
||||
);
|
||||
const vegetable2 = computed<string>(() =>
|
||||
randomElement(Object.keys(VEGETABLES), config.value.seed + 1),
|
||||
);
|
||||
const startTimeMinute = computed<number>(() =>
|
||||
timeToMinute(config.value.startTime),
|
||||
);
|
||||
const endTimeMinute = computed<number>(() => {
|
||||
const result = timeToMinute(config.value.endTime);
|
||||
return result < startTimeMinute.value ? result + 1440 : result;
|
||||
});
|
||||
|
||||
const candidates = computed<string[]>(() =>
|
||||
config.value.candidates
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(
|
||||
(value, index, array) =>
|
||||
value.length && array.indexOf(value) === index,
|
||||
),
|
||||
);
|
||||
|
||||
function generateData() {
|
||||
table.value.splice(0, table.value.length);
|
||||
if (candidates.value.length <= 2) {
|
||||
return;
|
||||
}
|
||||
const duration = parseInt(config.value.duration, 10);
|
||||
const mixThreshold = parseInt(config.value.mix, 10) / 100;
|
||||
const slots = [];
|
||||
for (
|
||||
let currentTimeMinute = startTimeMinute.value;
|
||||
currentTimeMinute < endTimeMinute.value;
|
||||
currentTimeMinute += duration
|
||||
) {
|
||||
slots.push(formatTime(currentTimeMinute));
|
||||
}
|
||||
|
||||
const generator = new TableGenerator(
|
||||
candidates.value,
|
||||
mixThreshold,
|
||||
slots,
|
||||
config.value.seed,
|
||||
);
|
||||
|
||||
const newTable = generator.generate();
|
||||
|
||||
if (config.value.endWithAll && newTable.length > 0) {
|
||||
newTable.splice(-1, 1, [slots.slice(-1)[0] ?? "?", "🥗 SALAD 🥗"]);
|
||||
}
|
||||
|
||||
table.value.push(...newTable);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
visible.value = true;
|
||||
});
|
||||
document.title = `${vegetable.value} Légume`;
|
||||
});
|
||||
|
||||
watch(vegetable, () => {
|
||||
document.title = `${vegetable.value} Légume`;
|
||||
});
|
||||
|
||||
watch(config, generateData, { deep: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main :style="{ display: visible ? 'inherit' : 'none' }">
|
||||
<!-- TODO: 1. rename app -->
|
||||
<h1>
|
||||
<LucideIcon name="package" />
|
||||
Vue-Boilerplate
|
||||
</h1>
|
||||
<h1>{{ vegetable }} Legume</h1>
|
||||
<br />
|
||||
<p>
|
||||
Fill this page with <i>whatever</i> you're going to develop.
|
||||
<br />
|
||||
<b>Then enjoy!</b>
|
||||
</p>
|
||||
<CustomButton>
|
||||
<LucideIcon name="square-arrow-right" /> This is a sample button yay
|
||||
</CustomButton>
|
||||
<ConfigTable v-model="config" />
|
||||
<template v-if="table.length">
|
||||
<h2>{{ vegetable2 }} Output (Vege)Table</h2>
|
||||
<OutputTable v-model="table" />
|
||||
</template>
|
||||
<br />
|
||||
<hr />
|
||||
<small class="footer">
|
||||
@@ -35,10 +99,9 @@ onMounted(() => {
|
||||
|
||||
<a href="https://github.com/klemek" target="_blank">klemek</a>
|
||||
-
|
||||
<!-- TODO: 1. rename app -->
|
||||
<LucideIcon name="github" />
|
||||
|
||||
<a href="https://github.com/klemek/vue-boilerplate" target="_blank">
|
||||
<a href="https://github.com/klemek/legume" target="_blank">
|
||||
Repository
|
||||
</a>
|
||||
- 2025
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { DEFAULT_CONFIG, VEGETABLES } from "@/contants";
|
||||
import { type Config } from "@/interfaces";
|
||||
import { getDataCookie, setCookie } from "@/lib/cookies";
|
||||
import { randomSeed, shuffleSeeded } from "@/lib/random";
|
||||
import { formatTime, timeToMinute } from "@/lib/time";
|
||||
import { computed, onMounted, watch } from "vue";
|
||||
import LucideIcon from "./LucideIcon.vue";
|
||||
|
||||
const config = defineModel<Config>({ required: true });
|
||||
|
||||
const candidates = computed<string[]>(() =>
|
||||
config.value.candidates
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(
|
||||
(value, index, array) =>
|
||||
value.length && array.indexOf(value) === index,
|
||||
),
|
||||
);
|
||||
|
||||
const startTimeMinute = computed<number>(() =>
|
||||
timeToMinute(config.value.startTime),
|
||||
);
|
||||
const endTimeMinute = computed<number>(() => {
|
||||
const result = timeToMinute(config.value.endTime);
|
||||
return result < startTimeMinute.value ? result + 1440 : result;
|
||||
});
|
||||
const totalDuration = computed<number>(
|
||||
() => endTimeMinute.value - startTimeMinute.value,
|
||||
);
|
||||
|
||||
const slotTooBig = computed<boolean>(() => {
|
||||
const slotCount = Math.ceil(
|
||||
totalDuration.value / parseInt(config.value.duration, 10),
|
||||
);
|
||||
|
||||
if (config.value.endWithAll) {
|
||||
return slotCount - 1 < candidates.value.length;
|
||||
}
|
||||
|
||||
return slotCount < candidates.value.length;
|
||||
});
|
||||
|
||||
function newSeed() {
|
||||
config.value.seed = randomSeed();
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
setCookie("legume-config", JSON.stringify(config.value));
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
config.value = getDataCookie("legume-config", DEFAULT_CONFIG);
|
||||
}
|
||||
|
||||
function newVegetables() {
|
||||
config.value.candidates = shuffleSeeded(
|
||||
Object.keys(VEGETABLES),
|
||||
randomSeed(),
|
||||
)
|
||||
.map((key) => `${key} ${VEGETABLES[key] ?? "?"}`)
|
||||
.slice(0, 6)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
newVegetables();
|
||||
loadConfig();
|
||||
});
|
||||
|
||||
watch(config, saveConfig, { deep: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="config">
|
||||
<colgroup>
|
||||
<col style="width: 25%" />
|
||||
<col />
|
||||
<col style="width: 25%" />
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td><label for="start-time">Start time:</label></td>
|
||||
<td>
|
||||
<input id="start-time" v-model="config.startTime" type="time" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="end-time">End time:</label></td>
|
||||
<td>
|
||||
<input id="end-time" v-model="config.endTime" type="time" />
|
||||
</td>
|
||||
<td>Total: {{ formatTime(totalDuration) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="duration">Slot duration:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="duration"
|
||||
v-model="config.duration"
|
||||
type="range"
|
||||
min="5"
|
||||
:max="totalDuration"
|
||||
step="5"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="slotTooBig" title="slot duration might be too big">
|
||||
<LucideIcon name="triangle-alert" /> {{ config.duration }} minutes
|
||||
</span>
|
||||
<span v-else> {{ config.duration }} minutes </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="seed">Seed:</label></td>
|
||||
<td><input v-model="config.seed" type="number" /></td>
|
||||
<td>
|
||||
<button @click="newSeed"><LucideIcon name="dices" /></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="mix">Mix policy:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="mix"
|
||||
v-model="config.mix"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="parseInt(config.mix, 10) <= 0">None</span>
|
||||
<span v-else>~{{ config.mix }}%</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="candidates">Candidates:</label></td>
|
||||
<td>
|
||||
<textarea
|
||||
id="candidates"
|
||||
v-model="config.candidates"
|
||||
rows="8"
|
||||
></textarea>
|
||||
</td>
|
||||
<td>
|
||||
<button @click="newVegetables"><LucideIcon name="dices" /></button>
|
||||
<br />
|
||||
<span
|
||||
v-if="candidates.length <= 2"
|
||||
title="not enough candidates"
|
||||
>
|
||||
<LucideIcon name="triangle-alert" /> <LucideIcon name="users-round" />
|
||||
{{ candidates.length }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<LucideIcon name="users-round" /> {{ candidates.length }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<button
|
||||
class="full"
|
||||
title="Ends event with all candidates (aka salad)"
|
||||
@click="config.endWithAll = !config.endWithAll"
|
||||
>
|
||||
🥗 With salad: {{ config.endWithAll ? "✅" : "❌" }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
table.config td {
|
||||
padding: 0.25em 0.5em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.config input,
|
||||
table.config select,
|
||||
button.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.config td:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
href?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="href ? 'a' : 'div'"
|
||||
:class="`button ${color ? 'b-' + color + ' ' + color : ''}`"
|
||||
>
|
||||
<slot></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
padding: 1em;
|
||||
margin-bottom: 0.75em;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--background);
|
||||
cursor: pointer;
|
||||
font-size: 1.333em;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import LucideIcon from "./LucideIcon.vue";
|
||||
|
||||
const table = defineModel<[string, string][]>({ required: true });
|
||||
|
||||
const copyTableOverride = ref<string | null>(null);
|
||||
|
||||
async function copyTable() {
|
||||
const csvTable = table.value.map((row) => row.join("\t")).join("\n");
|
||||
try {
|
||||
await navigator.clipboard.writeText(csvTable);
|
||||
copyTableOverride.value = "Table Copied";
|
||||
} catch {
|
||||
copyTableOverride.value = "Error";
|
||||
}
|
||||
setTimeout(() => {
|
||||
copyTableOverride.value = null;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table id="output" class="output">
|
||||
<colgroup>
|
||||
<col style="width: 25%" />
|
||||
<col />
|
||||
</colgroup>
|
||||
<tr v-for="(row, index) in table" :key="`out-row-${index}`">
|
||||
<td
|
||||
v-for="(item, index2) in row"
|
||||
:key="`out-cell-${index}-${index2}`"
|
||||
>
|
||||
{{ item }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
<p>
|
||||
<button @click="copyTable">
|
||||
<LucideIcon name="table" />
|
||||
<span v-if="copyTableOverride">{{ copyTableOverride }}</span>
|
||||
<span v-else>Copy table</span>
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
table.output,
|
||||
table.output th,
|
||||
table.output td {
|
||||
border: 1px solid var(--text-secondary);
|
||||
}
|
||||
|
||||
table.output td {
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
table.output td:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { type Config } from "./interfaces";
|
||||
import { randomSeed } from "./lib/random";
|
||||
|
||||
export const VEGETABLES: Record<string, string> = {
|
||||
"🥦": "Broccoli",
|
||||
"🥕": "Carrot",
|
||||
"🧅": "Onion",
|
||||
"🌶️": "Pepper",
|
||||
"🍆": "Eggplant",
|
||||
"🥔": "Potato",
|
||||
"🍄": "Mushroom",
|
||||
"🧄": "Garlic",
|
||||
"🥬": "Lettuce",
|
||||
"🥒": "Cucumber",
|
||||
"🥑": "Avocado",
|
||||
"🌽": "Corn",
|
||||
"🫘": "Beans",
|
||||
"🫚": "Ginger",
|
||||
"🫛": "Pea",
|
||||
"": "Radish",
|
||||
};
|
||||
|
||||
export const DEFAULT_CONFIG: Config = {
|
||||
startTime: "21:00",
|
||||
endTime: "03:00",
|
||||
duration: "30",
|
||||
seed: randomSeed(),
|
||||
candidates: "",
|
||||
endWithAll: true,
|
||||
mix: "25",
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface Config {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
duration: string;
|
||||
seed: number;
|
||||
candidates: string;
|
||||
endWithAll: boolean;
|
||||
mix: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export function setCookie(name: string, value: string, days = 30) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
const expires = `expires=${date.toUTCString()}`;
|
||||
document.cookie = `${name}=${value}; path=/; ${expires}`;
|
||||
}
|
||||
|
||||
export function getCookie(name: string, defaultValue: string): string {
|
||||
const prefix = `${name}=`;
|
||||
const decodedCookie = decodeURIComponent(document.cookie);
|
||||
const cookies = decodedCookie.split(";");
|
||||
for (const cookie of cookies) {
|
||||
if (cookie.trim().startsWith(prefix)) {
|
||||
return cookie.trim().substring(prefix.length, cookie.length);
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export function getDataCookie<T extends object>(
|
||||
name: string,
|
||||
defaultValue: T,
|
||||
): T {
|
||||
const rawCookie = getCookie(name, "");
|
||||
if (rawCookie.length) {
|
||||
try {
|
||||
const parsedConfig = JSON.parse(rawCookie) as T;
|
||||
return { ...parsedConfig, ...defaultValue };
|
||||
} catch {
|
||||
/* Empty */
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function gcd(numA: number, numB: number): number {
|
||||
let tmpA = numA;
|
||||
let tmpB = numB;
|
||||
while (tmpA !== tmpB) {
|
||||
if (tmpA > tmpB) {
|
||||
tmpA -= tmpB;
|
||||
} else {
|
||||
tmpB -= tmpA;
|
||||
}
|
||||
}
|
||||
return tmpA;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export function randomSeed(): number {
|
||||
return (Math.random() * 2 ** 32) >>> 0;
|
||||
}
|
||||
|
||||
export function splitmix32(seed: number): () => number {
|
||||
let localSeed = seed;
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
export function randomInt(max: number, seed: number) {
|
||||
const prng = splitmix32(seed);
|
||||
return Math.floor(prng() * max);
|
||||
}
|
||||
|
||||
export function randomElement<T>(array: T[], seed: number): T {
|
||||
return array[randomInt(array.length, seed)] as T;
|
||||
}
|
||||
|
||||
export function shuffleSeeded<T>(array: T[], seed: number): T[] {
|
||||
const output = array.slice();
|
||||
const prng = 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);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const tmp = output[i2]!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
output[i2] = output[i1]!;
|
||||
output[i1] = tmp;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { shuffleSeeded, splitmix32 } from "./random";
|
||||
import { gcd } from "./math";
|
||||
|
||||
export class TableGenerator {
|
||||
candidates: string[];
|
||||
size: number;
|
||||
mixThreshold: number;
|
||||
slots: string[];
|
||||
prng: () => number;
|
||||
indexScores: Record<number, number>;
|
||||
minIndexScore: number;
|
||||
maxIndexScore: number;
|
||||
mixScores: Record<number, number>;
|
||||
minMixScore: number;
|
||||
maxMixScore: number;
|
||||
lastIndexes: number[];
|
||||
mixTable: boolean[];
|
||||
|
||||
constructor(
|
||||
candidates: string[],
|
||||
mixThreshold: number,
|
||||
slots: string[],
|
||||
seed: number,
|
||||
) {
|
||||
this.candidates = candidates;
|
||||
this.size = this.candidates.length;
|
||||
this.mixThreshold = mixThreshold;
|
||||
this.slots = slots;
|
||||
this.prng = 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(): Record<number, number> {
|
||||
return Object.fromEntries(
|
||||
this.candidates.map((line, index) => [index, 0]),
|
||||
);
|
||||
}
|
||||
|
||||
initMixScores(): Record<number, number> {
|
||||
const scores: Record<number, number> = {};
|
||||
|
||||
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: number): boolean[] {
|
||||
const mixSlots = Math.round(this.mixThreshold * this.slots.length);
|
||||
if (mixSlots <= 0) {
|
||||
return [false];
|
||||
}
|
||||
const g = gcd(mixSlots, this.slots.length);
|
||||
const mixTableLength = this.slots.length / g;
|
||||
const tmpMixTable = new Array(mixTableLength)
|
||||
.fill(0)
|
||||
.map((unused, index) => index < mixSlots / g);
|
||||
return shuffleSeeded(tmpMixTable, seed + 1);
|
||||
}
|
||||
|
||||
mixKey(index1: number, index2: number): number {
|
||||
return Math.min(index1, index2) * 1000 + Math.max(index1, index2);
|
||||
}
|
||||
|
||||
getRandomIndex(): number {
|
||||
return Math.floor(this.prng() * this.size);
|
||||
}
|
||||
|
||||
getRandomMix(): [number, number] {
|
||||
const index1 = this.getRandomIndex();
|
||||
let index2;
|
||||
do {
|
||||
index2 = this.getRandomIndex();
|
||||
} while (index1 === index2);
|
||||
return [index1, index2];
|
||||
}
|
||||
|
||||
updateIndexScores(index1: number, index2: number): void {
|
||||
for (let index = 0; index < this.size; index += 1) {
|
||||
if (index !== index1 && index !== index2) {
|
||||
this.indexScores[index] = (this.indexScores[index] ?? 0) - 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: number, index2: number): void {
|
||||
this.updateIndexScores(index1, index2);
|
||||
this.mixScores[this.mixKey(index1, index2)] =
|
||||
(this.mixScores[this.mixKey(index1, index2)] ?? 0) + 1;
|
||||
this.minMixScore = Math.min(...Object.values(this.mixScores));
|
||||
this.maxMixScore = Math.max(...Object.values(this.mixScores));
|
||||
}
|
||||
|
||||
indexScoreThreshold(value: number): number {
|
||||
return (
|
||||
this.minIndexScore +
|
||||
(this.maxIndexScore - this.minIndexScore) * value
|
||||
);
|
||||
}
|
||||
|
||||
mixScoreThreshold(value: number): number {
|
||||
return this.minMixScore + (this.maxMixScore - this.minMixScore) * value;
|
||||
}
|
||||
|
||||
getMixValue(): string | null {
|
||||
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] ?? 0) > indexScoreThreshold ||
|
||||
(this.indexScores[index2] ?? 0) > indexScoreThreshold ||
|
||||
(this.mixScores[this.mixKey(index1, index2)] ?? 0) >
|
||||
mixScoreThreshold) &&
|
||||
(retries -= 1) > 0
|
||||
);
|
||||
if (retries === 0) {
|
||||
return null;
|
||||
}
|
||||
this.updateMixScores(index1, index2);
|
||||
return `${this.candidates[index1] ?? "?"} & ${this.candidates[index2] ?? "?"}`;
|
||||
}
|
||||
|
||||
getCandidateValue(): string {
|
||||
const indexScoreThreshold = this.indexScoreThreshold(0.1);
|
||||
let retries = 500;
|
||||
let index;
|
||||
do {
|
||||
index = this.getRandomIndex();
|
||||
} while (
|
||||
(this.lastIndexes.includes(index) ||
|
||||
(this.indexScores[index] ?? 0) > indexScoreThreshold) &&
|
||||
(retries -= 1) > 0
|
||||
);
|
||||
this.updateIndexScores(index, -1);
|
||||
return this.candidates[index] ?? "?";
|
||||
}
|
||||
|
||||
getSlotValue(index: number): string {
|
||||
if (this.mixTable[index % this.mixTable.length]) {
|
||||
const value = this.getMixValue();
|
||||
if (value !== null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return this.getCandidateValue();
|
||||
}
|
||||
|
||||
generate(): [string, string][] {
|
||||
return this.slots.map((slot, index) => [
|
||||
slot,
|
||||
this.getSlotValue(index),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function formatTime(minutes: number): string {
|
||||
return `${Math.floor((minutes / 60) % 24)
|
||||
.toFixed(0)
|
||||
.padStart(2, "0")}:${(minutes % 60).toFixed(0).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function timeToMinute(time: string): number {
|
||||
return Math.floor(Date.parse(`1970-01-01T${time}:00Z`) / 60000);
|
||||
}
|
||||
Reference in New Issue
Block a user