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`;
}