Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### English
- **Two new achievements:** 🌋 **Trillion hash** (a share with difficulty over 1 trillion) and the 🏆 **Golden prize: block found!**, unlocked when a share passes the network difficulty. The golden prize sits on its own full-width row, shows its trophy even while locked and lights up in gold when won.

### Português
- **Duas conquistas novas:** 🌋 **Um trilhão** (um share com dificuldade acima de 1 trilhão) e o 🏆 **Prêmio de ouro: bloco achado!**, que desbloqueia quando um share passa da dificuldade da rede. O prêmio de ouro fica numa linha inteira só para ele, mostra o troféu mesmo trancado e acende em dourado quando sai.

## v0.6.0 — 2026-11-12

### English
Expand Down
7 changes: 7 additions & 0 deletions src/luck.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,21 @@ const ACHIEVEMENTS = [
{ id: 'diff_1k', icon: '🎯', pt: ['Clube dos 1.000', 'Um share com dificuldade acima de 1.000'], en: ['1,000 club', 'A share with difficulty over 1,000'] },
{ id: 'diff_1m', icon: '🚀', pt: ['Um em um milhão', 'Um share com dificuldade acima de 1 milhão'], en: ['One in a million', 'A share with difficulty over 1 million'] },
{ id: 'diff_1b', icon: '☄️', pt: ['Quase bloco (1 bi)', 'Um share com dificuldade acima de 1 bilhão'], en: ['Billionaire hash', 'A share with difficulty over 1 billion'] },
{ id: 'diff_1t', icon: '🌋', pt: ['Um trilhão', 'Um share com dificuldade acima de 1 trilhão'], en: ['Trillion hash', 'A share with difficulty over 1 trillion'] },
{ id: 'shares_1k', icon: '📦', pt: ['Mil tentativas', '1.000 shares aceitos na fazenda'], en: ['A thousand tries', '1,000 accepted shares on the farm'] },
{ id: 'shares_100k', icon: '🏭', pt: ['100 mil tentativas', '100.000 shares aceitos na fazenda'], en: ['100,000 tries', '100,000 accepted shares on the farm'] },
{ id: 'luck_2x', icon: '🍀', pt: ['Dobro da sorte', 'Worker com sorte de pelo menos 2× o esperado'], en: ['Double luck', 'A worker with at least 2× expected luck'] },
{ id: 'luck_10x', icon: '⚡', pt: ['Raio na cabeça (10×)', 'Worker com sorte de pelo menos 10× o esperado'], en: ['Lightning strike (10×)', 'A worker with at least 10× expected luck'] },
{ id: 'week_nonstop', icon: '🛡️', pt: ['Uma semana sem parar', '7 dias seguidos minerando sem cair'], en: ['One week nonstop', '7 days in a row mining without stopping'] },
{ id: 'underdog', icon: '🐢', pt: ['Troféu vovô', 'Líder da semana com menos de 10% do maior hashrate'], en: ['Underdog trophy', 'Weekly leader with under 10% of the top hashrate'] },
// o prêmio de ouro: um share acima da dificuldade da rede é um bloco
{ id: 'block', icon: '🏆', golden: true, pt: ['Prêmio de ouro: bloco achado!', 'Um share acima da dificuldade da rede: a fazenda achou um bloco'], en: ['Golden prize: block found!', 'A share above the network difficulty: the farm found a block'] },
];

// Avalia quais conquistas já foram atingidas e preserva as já desbloqueadas em `saved`.
function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) {
const best = Number(ctx.best) || 0;
const netDiff = Number(ctx.networkDifficulty) || 0;
const bestWorker = ctx.bestWorker && ctx.bestWorker !== '—' ? ctx.bestWorker : null;
const totalShares = Number(ctx.totalShares) || 0;
const workers = Array.isArray(ctx.workers) ? ctx.workers : [];
Expand All @@ -104,12 +108,14 @@ function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) {
diff_1k: () => (best >= 1e3 ? { worker: bestWorker } : null),
diff_1m: () => (best >= 1e6 ? { worker: bestWorker } : null),
diff_1b: () => (best >= 1e9 ? { worker: bestWorker } : null),
diff_1t: () => (best >= 1e12 ? { worker: bestWorker } : null),
shares_1k: () => (totalShares >= 1e3 ? { worker: null } : null),
shares_100k: () => (totalShares >= 1e5 ? { worker: null } : null),
luck_2x: () => (topLuck && topLuck.factor >= 2 ? { worker: topLuck.name } : null),
luck_10x: () => (topLuck && topLuck.factor >= 10 ? { worker: topLuck.name } : null),
week_nonstop: () => (nonstop ? { worker: nonstop.name } : null),
underdog: () => (isUnderdog ? { worker: leaderName } : null),
block: () => (netDiff > 0 && best >= netDiff ? { worker: bestWorker } : null),
};

const map = {};
Expand All @@ -130,6 +136,7 @@ function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) {
const list = ACHIEVEMENTS.map((a) => ({
id: a.id,
icon: a.icon,
golden: !!a.golden,
pt: a.pt,
en: a.en,
unlocked: !!map[a.id],
Expand Down
8 changes: 6 additions & 2 deletions src/painel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,7 @@ function snapshot(internal = false) {
const topRec = [...records].sort((a, b) => b.value - a.value)[0];
const ach = evaluateAchievements({
best,
networkDifficulty: pool.difficulty,
bestWorker: topRec ? topRec.worker : null,
totalShares: Math.max(proxyTotalShares, poolTotalShares),
workers: view,
Expand Down Expand Up @@ -1244,6 +1245,9 @@ const PAGE = `<!doctype html>
.ach-i { font-size:18px; line-height:1.2; }
.ach-t { font-size:13px; font-weight:600; color:var(--txt); }
.ach.on .ach-t { color:var(--gold); }
.ach.golden { grid-column:1 / -1; border-style:dashed; }
.ach.golden.on { border:2px solid #f2a900; background:linear-gradient(90deg, rgba(242,169,0,.18), rgba(242,169,0,.04)); box-shadow:0 0 18px rgba(242,169,0,.35); }
.ach.golden.on .ach-t { color:#f2a900; font-size:15px; }
.ach-d { font-size:11px; color:var(--dim); line-height:1.3; margin-top:2px; }
.hist-card .chart { height:240px; }
body.kiosk .luck-duo { flex:1; min-height:0; }
Expand Down Expand Up @@ -2131,11 +2135,11 @@ function renderGamification(j) {
const isEn = LOC !== 'pt-BR';
for (const a of list) {
const box = document.createElement('div');
box.className = 'ach' + (a.unlocked ? ' on' : '');
box.className = 'ach' + (a.unlocked ? ' on' : '') + (a.golden ? ' golden' : '');
const pair = (isEn ? a.en : a.pt) || a.pt || ['', ''];
const ico = document.createElement('span');
ico.className = 'ach-i';
ico.textContent = a.unlocked ? a.icon : '🔒';
ico.textContent = a.unlocked || a.golden ? a.icon : '🔒'; // o troféu de ouro aparece mesmo trancado, para dar vontade
const txt = document.createElement('div');
const t = document.createElement('div');
t.className = 'ach-t';
Expand Down
16 changes: 16 additions & 0 deletions tests/luck.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,19 @@ test('cobertura de todas as conquistas e edge cases de evaluateAchievements', ()
assert.ok(nonArrayRes.map.first_share);
assert.strictEqual(nonArrayRes.map.first_share.worker, null);
});

test('conquistas: 1 trilhão e o prêmio de ouro (bloco = share acima da dificuldade da rede)', () => {
const net = 148.4e12, now = Date.now();
const quase = evaluateAchievements({ best: 2e12, bestWorker: 'orangeone', networkDifficulty: net }, {}, now);
const ids = quase.newlyUnlocked.map((a) => a.id);
assert.ok(ids.includes('diff_1t'));
assert.ok(!ids.includes('block'));
const semRede = evaluateAchievements({ best: 2e14, bestWorker: 'orangeone' }, {}, now); // sem dificuldade conhecida, não inventa bloco
assert.ok(!semRede.newlyUnlocked.some((a) => a.id === 'block'));
const bloco = evaluateAchievements({ best: 150e12, bestWorker: 'orangeone', networkDifficulty: net }, quase.map, now);
assert.deepStrictEqual(bloco.newlyUnlocked.map((a) => a.id), ['block']);
assert.strictEqual(bloco.map.block.worker, 'orangeone');
const g = bloco.list.find((a) => a.id === 'block');
assert.ok(g.golden && g.unlocked);
assert.strictEqual(ACHIEVEMENTS[ACHIEVEMENTS.length - 1].id, 'block', 'o prêmio de ouro fica por último');
});
Loading