diff --git a/apps/web/src/components/LotteryWidget.vue b/apps/web/src/components/LotteryWidget.vue index 9fffa2c..fac8900 100644 --- a/apps/web/src/components/LotteryWidget.vue +++ b/apps/web/src/components/LotteryWidget.vue @@ -27,7 +27,7 @@ let timer: number | null = null; onMounted(() => { timer = window.setInterval(() => { lineIndex.value = (lineIndex.value + 1) % LOTTERY_LINES.length; - }, 7000); + }, 10_000); }); onUnmounted(() => { if (timer) clearInterval(timer); diff --git a/apps/web/src/components/MinerRace.vue b/apps/web/src/components/MinerRace.vue index 1bea5d8..15f131f 100644 --- a/apps/web/src/components/MinerRace.vue +++ b/apps/web/src/components/MinerRace.vue @@ -3,10 +3,11 @@ import { computed } from "vue"; import type { DatumSnapshot, HistoryPoint, MinerStat } from "../types"; import { useRotatingCopy } from "../composables/useRotatingCopy"; import { RACE_SCALE_NOTES, RACE_TITLES } from "../copy"; +import { multiple } from "../format"; const props = defineProps<{ snapshot: DatumSnapshot | null; history: HistoryPoint[] }>(); const raceTitle = useRotatingCopy(RACE_TITLES); -const scaleNote = useRotatingCopy(RACE_SCALE_NOTES, 7600); +const scaleNote = useRotatingCopy(RACE_SCALE_NOTES); const networkEh = computed(() => props.snapshot?.network.hashrateEh ?? 0); const totalThs = computed(() => props.snapshot?.pool.combinedHashrateThs ?? 0); const networkMultiple = computed(() => networkEh.value > 0 && totalThs.value > 0 ? (networkEh.value * 1_000_000) / totalThs.value : 0); @@ -67,7 +68,7 @@ const cosmicRows = computed(() => { name: "Total Bitcoin network", value: `${networkEh.value.toFixed(0)} EH/s`, width: 100, - joke: `${networkMultiple.value.toExponential(2)}x your shelf. rude but factual.`, + joke: `${multiple(networkMultiple.value)} your shelf. rude but factual.`, }, ]; }); diff --git a/apps/web/src/components/PoolHero.vue b/apps/web/src/components/PoolHero.vue index 365b599..ff67f04 100644 --- a/apps/web/src/components/PoolHero.vue +++ b/apps/web/src/components/PoolHero.vue @@ -3,6 +3,7 @@ import { computed } from "vue"; import type { DatumSnapshot } from "../types"; import { useRotatingCopy } from "../composables/useRotatingCopy"; import { BLOCK_LABELS, HASHRATE_LABELS, NETWORK_LABELS } from "../copy"; +import { multiple } from "../format"; const props = defineProps<{ snapshot: DatumSnapshot | null }>(); @@ -20,8 +21,8 @@ const sharesAccepted = computed(() => props.snapshot?.pool.sharesAccepted ?? 0); const sharesRejected = computed(() => props.snapshot?.pool.sharesRejected ?? 0); const blockHeight = computed(() => props.snapshot?.job.blockHeight ?? 0); const hashrateLabel = useRotatingCopy(HASHRATE_LABELS); -const blockLabel = useRotatingCopy(BLOCK_LABELS, 7200); -const networkLabel = useRotatingCopy(NETWORK_LABELS, 7800); +const blockLabel = useRotatingCopy(BLOCK_LABELS); +const networkLabel = useRotatingCopy(NETWORK_LABELS); const ageS = computed(() => { const t = props.snapshot?.fetchedAt; if (!t) return null; @@ -50,7 +51,7 @@ const ageS = computed(() => {
network is bigger by
-
{{ networkMultiple > 0 ? `${networkMultiple.toExponential(2)}x` : "—" }}
+
{{ networkMultiple > 0 ? multiple(networkMultiple) : "—" }}
subscribed
diff --git a/apps/web/src/components/Shameboard.vue b/apps/web/src/components/Shameboard.vue index 791b9e4..26b2e6b 100644 --- a/apps/web/src/components/Shameboard.vue +++ b/apps/web/src/components/Shameboard.vue @@ -1,6 +1,7 @@ @@ -216,7 +217,7 @@ function pct(n: number, digits = 4): string {
network bullying - {{ networkMultiple > 0 ? `${networkMultiple.toExponential(2)}x` : "collecting" }} + {{ networkMultiple > 0 ? multiple(networkMultiple) : "collecting" }} {{ networkEh > 0 ? `${compact(networkEh, 0)} EH/s globally. easy version: the planet brought a warehouse.` : "tx1138 mempool warming up" }}
diff --git a/apps/web/src/composables/useRotatingCopy.ts b/apps/web/src/composables/useRotatingCopy.ts index bfc7b50..d8ea34b 100644 --- a/apps/web/src/composables/useRotatingCopy.ts +++ b/apps/web/src/composables/useRotatingCopy.ts @@ -1,6 +1,6 @@ import { computed, onMounted, onUnmounted, ref } from "vue"; -export function useRotatingCopy(lines: readonly string[], intervalMs = 6500) { +export function useRotatingCopy(lines: readonly string[], intervalMs = 10_000) { const index = ref(0); let timer: number | null = null; diff --git a/apps/web/src/format.ts b/apps/web/src/format.ts new file mode 100644 index 0000000..c144f15 --- /dev/null +++ b/apps/web/src/format.ts @@ -0,0 +1,26 @@ +export function compactNumber(n: number, digits = 1): string { + if (!Number.isFinite(n)) return "-"; + if (Math.abs(n) >= 1e12) return `${(n / 1e12).toFixed(digits)}T`; + if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(digits)}B`; + if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(digits)}M`; + if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(digits)}K`; + return n.toFixed(digits); +} + +export function oneIn(probability: number): string { + if (!Number.isFinite(probability) || probability <= 0) return "basically never"; + return `1 in ${compactNumber(1 / probability, 1)}`; +} + +export function tinyPercentAsOdds(percent: number): string { + if (!Number.isFinite(percent) || percent <= 0) return "0%"; + const probability = percent / 100; + if (percent < 0.001) return oneIn(probability); + if (percent < 1) return `${percent.toFixed(4)}%`; + return `${percent.toFixed(2)}%`; +} + +export function multiple(n: number): string { + if (!Number.isFinite(n) || n <= 0) return "-"; + return `${compactNumber(n, 1)}x`; +} diff --git a/apps/web/src/strings.ts b/apps/web/src/strings.ts index babb176..125ece2 100644 --- a/apps/web/src/strings.ts +++ b/apps/web/src/strings.ts @@ -1,3 +1,5 @@ +import { oneIn } from "./format"; + // All user-facing copy. Lean into the futility — these are 4 hobby boards // trying to hit a 1-in-quadrillions lottery. Take the piss with affection. @@ -153,7 +155,7 @@ export const STALE_LINES = [ function formatPct(p: number): string { if (!isFinite(p) || p <= 0) return "ε"; if (p >= 0.01) return `${(p * 100).toFixed(4)}%`; - return `${(p * 100).toExponential(2)}%`; + return oneIn(p); } function humanYears(years: number): string { @@ -168,8 +170,8 @@ function humanYears(years: number): string { function ratioVsLightning(oddsPerDay: number): string { const lightning = 0.00000028; const r = oddsPerDay / lightning; - if (r < 0.001) return r.toExponential(1); + if (r < 0.001) return `about ${oneIn(1 / r)} as likely`; if (r < 1) return r.toFixed(3); if (r < 1000) return r.toFixed(1); - return r.toExponential(1); + return `${(r / 1000).toFixed(1)}K`; }