Mini game
// ==UserScript==
// @name DeepCo NekHack
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @description Mini game
// @match https://deepco.app/*
// @license MIT
// @grant none
// @icon https://www.google.com/s2/favicons?sz=64&domain=deepco.app
// ==/UserScript==
(function () {
'use strict';
const EXTENSION_ID = 'nek-hack';
const EXTENSION_LABEL = 'NekHack';
const EXTENSION_VERSION = (typeof GM !== 'undefined' && GM?.info?.script?.version) || '0.2.1';
const BUTTON_COLOR = '#ff0000';
const INJECT_TARGET = 'footer.footer>nav';
const INJECT_BEFORE_ID = 'tip-area';
const GRAPH_WRAPPER_ID = 'nek-graph-wrapper';
const SCREEN_WRAPPER_ID = 'nek-arcade-wrapper';
const SCREEN_CANVAS_ID = 'nek-arcade-screen';
const PANEL_ID = 'nek-arcade-panel';
const TRACK_ID = 'nek-arcade-risk-track';
const POLL_INTERVAL = 2000;
const BET_OPTIONS = [10, 100, 1000];
const RISK_MIN = 0;
const RISK_MAX = 10;
const minRiskChance = 0.33;
const maxRiskChance = 0.10;
const baseRTP = 0.7;
let creditsLabel;
let prefsJson = {};
try {
prefsJson = JSON.parse(localStorage.getItem('nek-arcade-prefs')) || {};
} catch (e) {
console.error(e);
}
const prefs = {
show: true,
...prefsJson,
};
function savePrefs() {
localStorage.setItem('nek-arcade-prefs', JSON.stringify(prefs));
}
let stateJson = {};
try {
const rawData = localStorage.getItem('nek-arcade-state');
if (rawData) {
const decodedData = atob(rawData);
stateJson = JSON.parse(decodedData);
}
} catch (e) {
console.error(e);
}
const state = {
credits: 0,
nekots: 0,
lastBrokenBlocksTotal: null,
bet: 10,
risk: 5,
lastResult: '',
...stateJson,
};
function saveState() {
try {
const jsonString = JSON.stringify(state);
const encodedData = btoa(jsonString);
localStorage.setItem('nek-arcade-state', encodedData);
} catch (e) {
console.error(e);
}
}
class TabLeader {
constructor(channelName, { onGainLeadership, onLoseLeadership } = {}) {
this._channel = new BroadcastChannel(channelName);
this._isLeader = false;
this._electTimeout = null;
this._onGain = onGainLeadership ?? (() => {});
this._onLose = onLoseLeadership ?? (() => {});
this._channel.onmessage = ({ data }) => {
if (data === 'takeover') {
clearTimeout(this._electTimeout);
this._electTimeout = null;
if (this._isLeader) {
this._isLeader = false;
this._onLose();
}
} else if (data === 'leader-gone') {
this._electTimeout = setTimeout(
() => this._claim(),
Math.random() * 150 + 50
);
}
};
window.addEventListener('beforeunload', () => this.destroy());
}
claim() {
this._claim();
}
get isLeader() {
return this._isLeader;
}
destroy() {
clearTimeout(this._electTimeout);
if (this._isLeader) {
this._isLeader = false;
this._channel.postMessage('leader-gone');
}
this._channel.close();
}
_claim() {
this._electTimeout = null;
this._isLeader = true;
this._channel.postMessage('takeover');
this._onGain();
}
}
let leader, isLeader = false;
function scrapeBrokenBlocks() {
const el = document.getElementById('tiles-defeated-badge')?.children[0];
if (!el) return null;
const parsed = parseInt(el.textContent.split('/')[0].replace(/[,.\s]/g, ''));
return isNaN(parsed) ? null : parsed;
}
function collectCredits() {
const total = scrapeBrokenBlocks();
if (total === null) return;
if (state.lastBrokenBlocksTotal === null) {
state.lastBrokenBlocksTotal = total;
saveState();
return;
}
const delta = total - state.lastBrokenBlocksTotal;
if (delta > 0) {
state.credits += delta;
state.lastBrokenBlocksTotal = total;
saveState();
onCreditsChanged();
} else if (delta < 0) {
state.lastBrokenBlocksTotal = total;
saveState();
}
}
function startCreditCollector() {
setInterval(() => {
if (!isLeader) return;
collectCredits();
}, POLL_INTERVAL);
}
const screenWrapper = document.createElement('div');
screenWrapper.id = SCREEN_WRAPPER_ID;
screenWrapper.className = 'card bg-base-200 border border-base-300 p-4 mb-4 mx-2';
screenWrapper.style.cssText = "background: #05070a; overflow: scroll";
let cursorOn = true;
let spinText = null;
let isSpinning = false;
const SPIN_CHARS = '0123456789#$%&@+-';
const SPIN_LENGTH = 10;
const SPIN_TICKS_MS = [35, 35, 40, 45, 55, 65, 80, 100];
function randomSpinChunk() {
let out = '';
for (let i = 0; i < SPIN_LENGTH; i++) {
out += SPIN_CHARS[Math.floor(Math.random() * SPIN_CHARS.length)];
}
return out;
}
function renderScreenUI() {
screenWrapper.innerHTML = `
<div id="${SCREEN_CANVAS_ID}" style="
background: #05070a;
color: #ff0000;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.3;
padding: 10px;
border-radius: 4px 4px 0 0;
overflow: hidden;
white-space: pre;
width: 400px;
margin:auto;
user-select: none;
"></div>
<div id="${PANEL_ID}" style="
background: #05070a;
border-radius: 0 0 4px 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #ff0000;
user-select: none;
width: 400px;
min-width: max-content;
margin:auto;
"></div>
`;
drawFrame();
renderControls();
}
function drawFrame() {
const screen = document.getElementById(SCREEN_CANVAS_ID);
if (!screen) return;
const width = 48;
const line = '+' + '-'.repeat(width - 2) + '+';
const empty = '|' + ' '.repeat(width - 2) + '|';
const centered = (text, noborders) => {
const tempEl = document.createElement('div');
tempEl.innerHTML = text;
const ctext = tempEl.textContent || tempEl.innerText || "";
const pad = Math.max(0, width - 2 - ctext.length);
const left = Math.floor(pad / 2);
const right = pad - left;
return (noborders ? ' ':'|') + ' '.repeat(left) + text + ' '.repeat(right) + (noborders ? ' ':'|');
};
const resultLine = state.lastResult ? state.lastResult : '';
const resultHtml = isSpinning
? `<span style="color:#ff6600; opacity:0.85;">${spinText}</span>`
: `<span style="color:#ff6600;">${resultLine}${cursorOn ? '_' : ' '}</span>`;
const rows = [
centered('H A C K T E R M I N A L', true),
'',
line,
empty,
centered(`CREDS: <span style="color:#F22;">${state.credits}</span> GOLDS: <span style="color:#F22;">${state.nekots.toFixed(2)}</span>`),
empty,
centered(resultHtml),
empty,
line,
];
screen.innerHTML = rows.join('\n');
}
function onCreditsChanged() {
drawFrame();
creditsLabel.textContent = `Credits: ${state.credits}`
}
function trackString(value) {
let out = '[';
for (let i = RISK_MIN; i <= RISK_MAX; i++) {
out += i === value ? `-#-` : '---';
}
out += ']';
return out;
}
function setRisk(value) {
const clamped = Math.min(RISK_MAX, Math.max(RISK_MIN, Math.round(value)));
if (clamped === state.risk) return;
state.risk = clamped;
saveState();
drawFrame();
renderControls();
}
function selectBet(amount) {
state.bet = amount;
saveState();
drawFrame();
renderControls();
}
function winChance(risk) {
const x = risk / RISK_MAX;
const force = 10;
const logRatio = Math.log1p(force * x) / Math.log1p(force);
return minRiskChance - ((minRiskChance - maxRiskChance) * logRatio);
}
function calculateRTP(risk, bet) {
const riskBonus = 0.15 * (risk / RISK_MAX);
const minBet = Math.log10(BET_OPTIONS[0]);
const maxBet = Math.log10(BET_OPTIONS[BET_OPTIONS.length - 1]);
const betBonus = 0.1 * ((Math.log10(bet) - minBet) / (maxBet - minBet));
return baseRTP + riskBonus + betBonus;
}
function calculateGain(risk, bet) {
const chance = winChance(risk);
const rtp = calculateRTP(risk, bet);
const multiplier = (1 / chance) * (rtp+(1-rtp)/7.77);
const rawGain = bet * multiplier;
const pow = Math.pow(10,Math.floor(Math.log10(rawGain)));
const round = 10
return Math.floor(Math.ceil(round * bet * multiplier / pow)*pow/round);
}
function debugSimulate() {
console.group(`[NekHack Debug] Simulation du nouvel équilibrage`);
BET_OPTIONS.forEach((bet) => {
const tableData = [];
for (let risk = RISK_MIN; risk <= RISK_MAX; risk++) {
const chance = winChance(risk);
const gain = calculateGain(risk, bet);
const netGain = gain - bet;
const goldsGain = netGain / 100;
const expectedValue = (chance * gain) - bet;
const rtp = (chance * gain / bet) * 100;
tableData.push({
'Bet': bet,
'Risk': risk,
'Win Chance': `${(chance * 100).toFixed(1)}%`,
'Inverted Win Chance': `${(1/chance).toFixed(1)}`,
'Gain Brut': gain,
'Gain Net': netGain,
'Golds Net': goldsGain.toFixed(2),
'Golds conversion': (100 * chance * goldsGain / bet).toFixed(3),
'EV (Creds)': expectedValue.toFixed(2),
'RTP (%)': `${rtp.toFixed(1)}%`
});
}
console.table(tableData);
});
console.groupEnd();
}
function doConvert() {
if (state.credits < state.bet) {
state.lastResult = 'NOT ENOUGH CREDITS';
saveState();
drawFrame();
return;
}
state.credits -= state.bet;
const chance = winChance(state.risk);
const gain = calculateGain(state.risk, state.bet);
const roll = Math.random();
if (roll < chance) {
const ngain = (gain - state.bet) / 100;
state.credits += gain;
state.nekots += ngain;
state.lastResult = `+${gain} CREDS \\o/ +${ngain.toFixed(2)} GOLDS`;
} else {
state.lastResult = `LOST :(`;
}
saveState();
onCreditsChanged();
}
function playConvertAnimation(onDone) {
isSpinning = true;
let tickIndex = 0;
const tick = () => {
spinText = randomSpinChunk();
drawFrame();
if (tickIndex >= SPIN_TICKS_MS.length) {
isSpinning = false;
spinText = null;
onDone();
return;
}
setTimeout(tick, SPIN_TICKS_MS[tickIndex]);
tickIndex++;
};
tick();
}
function renderControls() {
const panel = document.getElementById(PANEL_ID);
if (!panel) return;
panel.innerHTML = '';
const betRow = document.createElement('div');
betRow.style.cssText = 'display: flex; gap: 6px; margin-bottom: 8px;justify-content: center';
const betLabel = document.createElement('span');
betLabel.textContent = 'BET';
betLabel.style.cssText = `
font-family: 'Courier New', monospace;
font-size: 13px;
padding: 4px 0;
`;
betRow.appendChild(betLabel);
BET_OPTIONS.forEach((amount) => {
const btn = document.createElement('button');
const active = state.bet === amount;
btn.textContent = active ? `[ ${amount} ]` : `\u00A0\u00A0${amount}\u00A0\u00A0`;
btn.style.cssText = `
font-family: 'Courier New', monospace;
font-size: 13px;
padding: 4px 0;
border-radius: 4px;
cursor: pointer;
color: ${active ? '#f00' : '#800'};
`;
btn.onclick = () => selectBet(amount);
betRow.appendChild(btn);
});
panel.appendChild(betRow);
const trackRow = document.createElement('div');
trackRow.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 8px; justify-content: center';
const trackLabel = document.createElement('span');
trackLabel.textContent = 'RISK';
trackRow.appendChild(trackLabel);
const track = document.createElement('div');
track.id = TRACK_ID;
track.textContent = trackString(state.risk);
track.style.cssText = 'flex: 1; letter-spacing: 1px; cursor: pointer; white-space: pre; flex-grow:0';
const valueFromEvent = (e, rect) => {
const charWidth = rect.width / trackString(state.risk).length;
const col = Math.floor((e.clientX - rect.left) / (charWidth*3));
return RISK_MIN + col;
};
track.addEventListener('mousedown', (e) => {
const rect = track.getBoundingClientRect();
setRisk(valueFromEvent(e,rect));
const onMove = (ev) => setRisk(valueFromEvent(ev, rect));
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
trackRow.appendChild(track);
panel.appendChild(trackRow);
const convertBtn = document.createElement('button');
convertBtn.textContent = '[ CONVERT ]';
convertBtn.style.cssText = `
width: 100%;
font-family: 'Courier New', monospace;
font-size: 13px;
padding: 5px 0;
cursor: pointer;
background: transparent;
color: #ff0000;
`;
if (isSpinning) {
convertBtn.disabled = true;
convertBtn.style.opacity = '0.5';
convertBtn.style.cursor = 'default';
}
convertBtn.onclick = () => {
if (isSpinning) return;
if (state.credits < state.bet) {
doConvert();
renderControls();
return;
}
isSpinning = true;
renderControls();
playConvertAnimation(() => {
doConvert();
renderControls();
});
};
panel.appendChild(convertBtn);
}
function startBlinkLoop() {
setInterval(() => {
if (!prefs.show) return;
cursorOn = !cursorOn;
drawFrame();
}, 600);
}
renderScreenUI();
if (!prefs.show) {
screenWrapper.classList.add('hidden');
}
function startScreenObserver() {
function tryInject() {
const target = document.getElementById(INJECT_BEFORE_ID);
if (!target) return;
const parent = target.parentNode;
if (screenWrapper.parentElement === parent) return;
const graphWrapper = document.getElementById(GRAPH_WRAPPER_ID);
if (graphWrapper && graphWrapper.parentElement === parent) {
parent.insertBefore(screenWrapper, graphWrapper.nextSibling);
} else {
parent.insertBefore(screenWrapper, target);
}
drawFrame();
renderControls();
}
tryInject();
new MutationObserver(tryInject).observe(document.documentElement, { childList: true, subtree: true });
}
function createMenuContent() {
const container = document.createElement('div');
container.style.cssText = 'padding: 5px 0;';
creditsLabel = document.createElement('div');
creditsLabel.textContent = `Credits: ${state.credits}`;
container.appendChild(creditsLabel);
const toggleWrapper = document.createElement('label');
toggleWrapper.classList.add('flex', 'items-center', 'gap-2', 'my-3');
toggleWrapper.dataset.tip = 'Afficher / masquer l\'écran';
const toggleLabel = document.createElement('span');
toggleLabel.textContent = 'Show Screen';
const toggleSwitch = document.createElement('input');
toggleSwitch.type = 'checkbox';
toggleSwitch.classList.add('toggle', 'toggle-primary', 'checkbox-xs');
toggleSwitch.checked = prefs.show;
toggleSwitch.onchange = () => {
prefs.show = toggleSwitch.checked;
savePrefs();
screenWrapper.classList.toggle('hidden', !prefs.show);
};
toggleWrapper.appendChild(toggleSwitch);
toggleWrapper.appendChild(toggleLabel);
container.appendChild(toggleWrapper);
const versionNote = document.createElement('div');
versionNote.textContent = `${EXTENSION_LABEL} v${EXTENSION_VERSION}`;
versionNote.style.cssText = 'font-size: 10px; color: #777; text-align: right; padding: 4px 10px 2px; pointer-events: none;';
container.appendChild(versionNote);
return container;
}
function createStandaloneWidget() {
const wrapper = document.createElement('div');
wrapper.style.cssText = 'position: relative; display: inline-block;';
const btn = document.createElement('button');
btn.textContent = EXTENSION_LABEL;
btn.style.color = BUTTON_COLOR;
btn.classList.add('btn', 'btn-ghost', 'btn-sm', 'text-primary');
const popup = document.createElement('div');
popup.style.cssText = 'display: none; position: absolute; bottom: calc(100% + 8px); left: 0; min-width: 200px; z-index: 1000; text-align: left;';
popup.classList.add('card', 'bg-base-100', 'border', 'border-base-300', 'p-2', 'text-xs', 'shadow-xl');
popup.appendChild(createMenuContent());
btn.addEventListener('click', (e) => {
e.stopPropagation();
popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
});
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) popup.style.display = 'none';
});
wrapper.append(btn, popup);
return wrapper;
}
function injectStandaloneButton() {
const widget = createStandaloneWidget();
function tryInject() {
const target = document.querySelector(INJECT_TARGET);
if (target && widget.parentElement !== target) target.insertBefore(widget, target.firstChild);
}
tryInject();
new MutationObserver(tryInject).observe(document.documentElement, { childList: true, subtree: true });
}
function registerWithNekStyle() {
window.NekStyle.registerExtension({
id: EXTENSION_ID,
label: EXTENSION_LABEL,
color: BUTTON_COLOR,
createContent: createMenuContent,
});
}
function init() {
//window.nekDebugSimulate = debugSimulate;
startScreenObserver();
startBlinkLoop();
leader = new TabLeader('nek-arcade-leader', {
onGainLeadership: () => {
isLeader = true;
},
onLoseLeadership: () => {
isLeader = false;
},
});
leader.claim();
startCreditCollector();
if (typeof window.NekStyle?.registerExtension === 'function') {
registerWithNekStyle();
return;
}
let registered = false;
window.__NekStyleExtensions = window.__NekStyleExtensions || [];
window.__NekStyleExtensions.push(() => { registered = true; registerWithNekStyle(); });
setTimeout(() => { if (!registered) injectStandaloneButton(); }, 0);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();