// ==UserScript== // @name Faucet Dice Analyzer v17.13 FAUCETMATRIX // @namespace dice-analyzer // @version 17.13 // @description Faucet Dice Analyzer for ultimate dice game // @match *://*.freeshib.in/* // @match *://*.freetron.in/* // @match *://*.freebnb.in/* // @match *://*.freebch.in/* // @match *://*.freetroncoin.in/* // @match *://*.freetrump.in/* // @match *://*.usdpick.io/* // @match *://*.freebtcco.in/* // @match *://*.freearb.in/* // @match *://*.dogedrop.co/* // @run-at document-idle // @license MIT // @grant none // ==/UserScript== (function(){ 'use strict'; // ==================== CONFIG ==================== var SITE = location.hostname; var ZONE_WIDTH = 1; var ZONE_COUNT = Math.floor(100 / ZONE_WIDTH); // Storage keys (v17.2 — fresh, imports from v17) var SK_HIST = 'dv172_h_' + SITE; var SK_CHAINS = 'dv172_c_' + SITE; var SK_MODEL = 'dv172_m_' + SITE; var SK_STATE = 'dv172_s_' + SITE; var SK_PLOG = 'dv172_p_' + SITE; var SK_MACROS = 'dv172_mc_' + SITE; var SK_GHOSTS = 'dv172_gh_' + SITE; // v17.12: SK_UCHAINS removed — user-chain module deleted. // Core SEED zones var SEED = [0,11,14,22,33,42,44,46,49,50,52,55,66,77,86,88,89,92,99]; var SEED_SET = {}; for (var si = 0; si < SEED.length; si++) SEED_SET[SEED[si]] = true; // Chain config var NGRAMS = [1, 2, 3]; var MIN_OBS = 2; var MAX_CHAINS = 10000; var MAX_GAP_REC = 250; var GAP_KEEP = 20; // Lifecycle var LF_WIN = 80; var LF_PAT_RATE = 0.17; var LF_CHAOS_RATE = 0.10; var LF_ENTER_DUR = 4; var LF_EXIT_DUR = 10; // Dynamic zone discovery var DYN_WIN = 150; var DYN_MIN = 5; var DYN_MULT = 2.0; // Burst var BURST_WIN = 15; var BURST_MIN = 3; var BURST_BOOST = 25; // Prediction queue var QUEUE_SIZE = 4; var PRD_MIN_WAIT = 15; var PRD_MAX_WAIT = 55; var PRD_WAIT_MULT = 2.0; var PRD_MIN_CF = 18; var ZONE_SPREAD = 1; var DELAYED_EXT = 35; // v17: was 30, extended by 5 // Queue stability var IMMUNITY_FRAC = 0.6; var REPLACE_CF_BOOST = 25; var NO_REBUILD_PERIOD = 12; // Absent zone penalty var ABSENT_PENALTY_R1 = 200; var ABSENT_PENALTY_R2 = 400; var ABSENT_PENALTY_FACTOR = 0.3; // Non-SEED confidence gate var NON_SEED_CF_MULT = 2.0; // Macro config var MACRO_MAX = 600; // v17: was 500, more capacity var MACRO_MIN_LEN = 5; var MACRO_MAX_LEN = 10; var MACRO_PREFIX = [2, 3, 4]; // v17: added 4-gram prefixes var MACRO_MAX_AGE = 15000; var MACRO_MIN_OBS = 3; var MACRO_SCAN_LEN = 50; // v17: was 30, wider scan // NEW v17: Overdelayed ghost tracking var OVERDELAYED_WINDOW = 30; // track for 30 rolls after expiry var MAX_GHOSTS = 10; // max concurrent ghosts // NEW v17: Chaos interrupt detection var CHAOS_BOUNDARIES = [0, 14, 88, 92, 99]; var CHAOS_BOUNDARY_SET = {}; for (var ci = 0; ci < CHAOS_BOUNDARIES.length; ci++) CHAOS_BOUNDARY_SET[CHAOS_BOUNDARIES[ci]] = true; var CHAOS_CLUSTER_WIN = 5; // 2+ boundaries within 5 rolls var CHAOS_MEMORY = 25; // chaos flag lasts 25 rolls var CHAOS_DELAY_BONUS = 15; // bonus mw when chaos detected var CHAOS_MIN_CLUSTER = 2; // minimum boundaries for chaos event // NEW v17.2: Drift corrector config var DRIFT_LR = 0.15; // learning rate (v17 was 0.02) var DRIFT_CLAMP = 30; // max correction in either direction var DRIFT_EXPECTED = 0.6; // expected hit as fraction of mw var DRIFT_MIN_OBS = 3; // need 3+ resolved preds before applying global drift // NEW v17.2: Anti-premature-chaos config var SEED_RECENT_WIN = 8; // if SEED seen within this many rolls, don't drop to ENTER // v17.12: USER_CHAIN_* config removed. // NEW v17.7: Prediction cooldown — prevents re-predicting the same zone // immediately after it expired. Forces the engine to pick a different // zone, breaking the "stuck on zone X" loop. var PRED_COOLDOWN_ROLLS = 25; // rolls to skip a zone after expiry var PRED_COOLDOWN_HIT_CLEAR = true; // clear cooldown immediately on HIT // NEW v17.7: Flow window config var FLOW_PANEL_COUNT_DEFAULT = 40; // default visible notable zones var FLOW_PANEL_COUNT_MIN = 10; var FLOW_PANEL_COUNT_MAX = 200; var FLOW_LONG_GAP_DEFAULT = 15; // default long-gap threshold (rolls) var FLOW_LONG_GAP_MIN = 5; var FLOW_LONG_GAP_MAX = 100; var FLOW_REFRESH_MS = 2000; // History var MAX_HIST = 5000; var MAX_NOTABLE = 600; // ==================== STATE ==================== var history = ld(SK_HIST, []); var chains = ld(SK_CHAINS, {}); var model = ld(SK_MODEL, {}); var st = ld(SK_STATE, {}); var plog = ld(SK_PLOG, []); var macros = ld(SK_MACROS, {}); var ghosts = ld(SK_GHOSTS, []); // NEW v17: overdelayed ghosts // NEW v17.2: Drift corrector state var globalDrift = st.gd || 0; // global drift offset (positive = predictions late) var zoneDrift = st.zd || {}; // per-zone drift {zone: drift_value} var driftObs = st.do || 0; // number of observations for global drift // v17.12: userChains removed (module deleted). var rollCounter = st.rc || history.length; var lifecycle = st.lf || 'CHAOS'; var lifeCount = st.lc || 0; var notableSeq = st.ns || []; var dynZones = st.dz || {}; var burstZones = st.bz || {}; var predQueue = st.pq || []; var lastRebuild = st.lr || 0; var chaosTimer = st.ct || 0; // NEW v17: chaos countdown var chaosCount = st.cc || 0; // NEW v17: total chaos events // NEW v17.7: Prediction cooldown state // Map: { zone: rollCounter_when_cooldown_started } // A zone is in cooldown if (rollCounter - predCooldown[zone]) < PRED_COOLDOWN_ROLLS var predCooldown = st.pcd || {}; // NEW v17.7: Flow window state (persisted so user's preference survives reloads) var flowPanelCount = st.fpc || FLOW_PANEL_COUNT_DEFAULT; var flowLongGap = st.flg || FLOW_LONG_GAP_DEFAULT; var overlay; var flowOverlay = null; var flowRefreshInterval = null; // ==================== STORAGE ==================== function ld(k, d) { try { var v = JSON.parse(localStorage.getItem(k)); return (v !== null && v !== undefined) ? v : d; } catch(e) { return d; } } function sv() { try { localStorage.setItem(SK_HIST, JSON.stringify(history)); localStorage.setItem(SK_CHAINS, JSON.stringify(chains)); localStorage.setItem(SK_MODEL, JSON.stringify(model)); localStorage.setItem(SK_MACROS, JSON.stringify(macros)); st = { rc: rollCounter, lf: lifecycle, lc: lifeCount, ap: null, ns: notableSeq.slice(-MAX_NOTABLE), dz: dynZones, bz: burstZones, pq: predQueue.slice(-QUEUE_SIZE), lr: lastRebuild, ct: chaosTimer, cc: chaosCount, gd: globalDrift, zd: zoneDrift, do: driftObs, pcd: predCooldown, fpc: flowPanelCount, flg: flowLongGap }; localStorage.setItem(SK_STATE, JSON.stringify(st)); if (plog.length > 4000) plog = plog.slice(-4000); localStorage.setItem(SK_PLOG, JSON.stringify(plog)); localStorage.setItem(SK_GHOSTS, JSON.stringify(ghosts.slice(-MAX_GHOSTS))); } catch(e) { trimChains(); trimMacros(); try { sv(); } catch(e2) {} } } function trimChains() { var keys = Object.keys(chains); if (keys.length <= MAX_CHAINS) return; keys.sort(function(a, b) { return (chains[a]._t || 0) - (chains[b]._t || 0); }); for (var i = 0; i < keys.length - MAX_CHAINS; i++) delete chains[keys[i]]; } function trimMacros() { var keys = Object.keys(macros); if (keys.length <= MACRO_MAX) return; keys.sort(function(a, b) { return getMacroAge(a) - getMacroAge(b); }); for (var i = 0; i < keys.length - MACRO_MAX; i++) delete macros[keys[i]]; } function getMacroAge(key) { var entries = macros[key]; if (!entries || !entries.length) return 999999; var maxLast = 0; for (var i = 0; i < entries.length; i++) { if (entries[i].last > maxLast) maxLast = entries[i].last; } return rollCounter - maxLast; } // ==================== ZONE DETECTION ==================== function detectZone(v) { var z = Math.floor(v / ZONE_WIDTH); return (z >= 0 && z < ZONE_COUNT) ? z : null; } // ==================== CHAIN ALPHABET ==================== function isChainable(zone) { if (zone === null) return false; return SEED_SET[zone] || dynZones.hasOwnProperty(String(zone)); } function computeDynZones() { var counts = {}; var ws = Math.max(0, history.length - DYN_WIN); var total = 0; for (var i = ws; i < history.length; i++) { var z = history[i].zone; if (z !== null) { counts[z] = (counts[z] || 0) + 1; total++; } } var expected = total > 0 ? total / ZONE_COUNT : 0; var result = {}; for (var k in counts) { var zk = parseInt(k); if (!SEED_SET[zk] && counts[k] >= DYN_MIN && counts[k] >= expected * DYN_MULT) { result[k] = counts[k]; } } return result; } // ==================== BURST DETECTION ==================== function updateBurstZones(zone) { if (zone === null) return; var recentHits = 0; for (var i = history.length - 1; i >= 0 && i >= history.length - BURST_WIN; i--) { if (history[i].zone === zone) recentHits++; } if (recentHits >= BURST_MIN) { if (!burstZones[zone]) burstZones[zone] = {count: 0, first: 0}; burstZones[zone].count = recentHits; burstZones[zone].first = rollCounter; } else if (burstZones[zone] && rollCounter - burstZones[zone].first > BURST_WIN) { delete burstZones[zone]; } } function isBurst(zone) { return zone !== null && burstZones.hasOwnProperty(String(zone)); } // ==================== CHAOS DETECTION (NEW v17) ==================== // Detects clusters of boundary zones (0,14,88,92,99) that // interrupt macro chains. After chaos, chains still complete // but with extra delay. var recentBoundaryRolls = []; // tracks recent boundary zone roll indices function detectChaos(zone) { if (zone === null || !CHAOS_BOUNDARY_SET[zone]) { // No boundary zone — just decay the timer if (chaosTimer > 0) chaosTimer--; return false; } // Record this boundary zone recentBoundaryRolls.push(rollCounter); // Keep only recent entries within CHAOS_CLUSTER_WIN while (recentBoundaryRolls.length > 0 && rollCounter - recentBoundaryRolls[0] > CHAOS_CLUSTER_WIN) { recentBoundaryRolls.shift(); } // Check if we have a cluster if (recentBoundaryRolls.length >= CHAOS_MIN_CLUSTER) { // Chaos event detected! if (chaosTimer <= 0) { // New chaos event (not extending existing) chaosCount++; } chaosTimer = CHAOS_MEMORY; // Extend active prediction's max wait if (predQueue.length > 0 && predQueue[0].status === 'active') { if (!predQueue[0].chaosExtended) { predQueue[0].mw += CHAOS_DELAY_BONUS; predQueue[0].chaosExtended = true; } } // Record chaos→next pattern in macro dict // (what comes after chaos clusters historically) recordChaosMacro(); return true; } if (chaosTimer > 0) chaosTimer--; return false; } function recordChaosMacro() { // Find the boundary zone cluster in notableSeq if (notableSeq.length < 3) return; // Look for the last 2+ boundary zones in notableSeq var clusterEnd = -1; var clusterStart = -1; var clusterCount = 0; for (var i = notableSeq.length - 1; i >= 0; i--) { if (CHAOS_BOUNDARY_SET[notableSeq[i].z]) { if (clusterEnd === -1) clusterEnd = i; clusterStart = i; clusterCount++; if (clusterCount >= CHAOS_MIN_CLUSTER) break; } else if (clusterCount > 0) { break; // gap in cluster } } if (clusterCount < CHAOS_MIN_CLUSTER || clusterStart < 1) return; // Build prefix from zones before the cluster var preLen = Math.min(2, clusterStart); var prefix = []; for (var i = clusterStart - preLen; i < clusterStart; i++) { if (i >= 0) prefix.push(notableSeq[i].z); } if (prefix.length === 0) return; // Build continuation from zones after the cluster var contStart = clusterEnd + 1; if (contStart >= notableSeq.length) return; var cont = []; var gaps = []; for (var i = contStart; i < notableSeq.length && cont.length < 5; i++) { if (i > contStart) { gaps.push(notableSeq[i].ri - notableSeq[i-1].ri); } cont.push(notableSeq[i].z); } if (cont.length === 0) return; // Store as special "CHAOS:" prefix macro var chaosPrefix = 'CHAOS:' + prefix.join(','); if (!macros[chaosPrefix]) macros[chaosPrefix] = []; var contStr = cont.join(','); var found = false; for (var j = 0; j < macros[chaosPrefix].length; j++) { if (macros[chaosPrefix][j].contStr === contStr) { macros[chaosPrefix][j].count++; macros[chaosPrefix][j].last = rollCounter; for (var gi = 0; gi < gaps.length; gi++) { macros[chaosPrefix][j].gaps.push(gaps[gi]); if (macros[chaosPrefix][j].gaps.length > GAP_KEEP * 3) macros[chaosPrefix][j].gaps.shift(); } found = true; break; } } if (!found) { macros[chaosPrefix].push({ contStr: contStr, cont: cont.slice(), gaps: gaps.slice(), count: 1, first: rollCounter, last: rollCounter }); } } function isInChaos() { return chaosTimer > 0; } // ==================== MODEL (with BIAS) ==================== function initModel() { for (var z = 0; z < ZONE_COUNT; z++) { if (!model[z]) model[z] = { h: 0, c: ZONE_COUNT, ls: 0, bias: 0 }; } } // ==================== ABSENT ZONE PENALTY ==================== function getAbsentPenalty(zone) { if (!model[zone] || model[zone].ls === 0) return 1.0; var absent = rollCounter - model[zone].ls; if (absent > ABSENT_PENALTY_R2) return 0; if (absent > ABSENT_PENALTY_R1) return ABSENT_PENALTY_FACTOR; return 1.0; } // ==================== DELAY FEEDBACK ==================== function updateBias(zone, predictedGap, actualDelay) { if (!model[zone]) return; var m = model[zone]; var error = actualDelay - predictedGap; m.bias = (m.bias || 0) + error * 0.02; if (m.bias > 4) m.bias = 4; if (m.bias < -4) m.bias = -4; } function applyBias(zone, eta) { if (!model[zone] || !model[zone].bias) return eta; return eta * (1 + model[zone].bias * 0.08); } // ==================== DRIFT CORRECTOR (NEW v17.2) ==================== function updateDrift(entry) { // Called when a prediction resolves (HIT/DELAYED/NEAR/OVERDELAYED/EXPIRED) if (!entry || !entry.rtr || !entry.mw) return; var expected = entry.mw * DRIFT_EXPECTED; var error = entry.rtr - expected; // positive = came late // Global drift if (driftObs < DRIFT_MIN_OBS) { globalDrift += error; } else { globalDrift = globalDrift * (1 - DRIFT_LR) + error * DRIFT_LR; } if (globalDrift > DRIFT_CLAMP) globalDrift = DRIFT_CLAMP; if (globalDrift < -DRIFT_CLAMP) globalDrift = -DRIFT_CLAMP; driftObs++; // Per-zone drift var z = entry.z; if (zoneDrift[z] === undefined) zoneDrift[z] = 0; zoneDrift[z] = zoneDrift[z] * (1 - DRIFT_LR) + error * DRIFT_LR; if (zoneDrift[z] > DRIFT_CLAMP) zoneDrift[z] = DRIFT_CLAMP; if (zoneDrift[z] < -DRIFT_CLAMP) zoneDrift[z] = -DRIFT_CLAMP; } function applyDrift(zone, eta) { // Apply global + per-zone drift correction to ETA if (driftObs < DRIFT_MIN_OBS) return eta; var correction = globalDrift + (zoneDrift[zone] || 0); // Drift is positive when predictions arrive late → reduce ETA // Drift is negative when predictions arrive early → increase ETA var corrected = eta - correction; return Math.max(1, corrected); } // ==================== LIFECYCLE ==================== function getSeedRate() { var ws = Math.max(0, history.length - LF_WIN); var seedHits = 0, total = 0; for (var i = ws; i < history.length; i++) { var z = history[i].zone; if (z !== null) { total++; if (SEED_SET[z]) seedHits++; } } return total > 0 ? seedHits / total : 0; } function updateLifecycle() { var rate = getSeedRate(); // v17.2 FIX 8: If a SEED zone appeared recently (within SEED_RECENT_WIN rolls), // don't let lifecycle drop below ACTIVE. This prevents killing predictions // during spread-out but real SEED chains where rate temporarily dips. var recentSeed = false; if (lifecycle === 'ACTIVE' || lifecycle === 'ENTER') { for (var ri = history.length - 1; ri >= 0 && ri >= history.length - SEED_RECENT_WIN; ri--) { if (history[ri].zone !== null && SEED_SET[history[ri].zone]) { recentSeed = true; break; } } } switch (lifecycle) { case 'CHAOS': if (rate >= LF_PAT_RATE) { lifecycle = 'ENTER'; lifeCount = 0; } break; case 'ENTER': lifeCount++; if (rate < LF_CHAOS_RATE && !recentSeed) { lifecycle = 'CHAOS'; lifeCount = 0; } else if (lifeCount >= LF_ENTER_DUR) { lifecycle = 'ACTIVE'; lifeCount = 0; } break; case 'ACTIVE': lifeCount++; if (rate < LF_CHAOS_RATE && !recentSeed) { lifecycle = 'EXIT'; lifeCount = 0; } break; case 'EXIT': lifeCount++; if (rate >= LF_PAT_RATE || recentSeed) { lifecycle = 'ACTIVE'; lifeCount = 0; } else if (lifeCount >= LF_EXIT_DUR) { lifecycle = 'CHAOS'; lifeCount = 0; for (var i = 0; i < predQueue.length; i++) { if (predQueue[i].status === 'active') { resolveQueueEntry(i, 'CANCEL'); } } predQueue = []; } break; } } // ==================== CHAIN DATABASE ==================== function addChainEntries(newZone) { if (notableSeq.length === 0) return; var gap = rollCounter - notableSeq[notableSeq.length - 1].ri; if (gap <= 0 || gap > MAX_GAP_REC) return; for (var ni = 0; ni < NGRAMS.length; ni++) { var K = NGRAMS[ni]; if (notableSeq.length < K) continue; var parts = []; for (var i = notableSeq.length - K; i < notableSeq.length; i++) parts.push(notableSeq[i].z); var key = parts.join(','); if (!chains[key]) chains[key] = { _t: 0 }; chains[key]._t++; var zk = String(newZone); if (!chains[key][zk]) chains[key][zk] = { g: [], n: 0, m: 0, s: 0 }; var e = chains[key][zk]; e.g.push(gap); if (e.g.length > GAP_KEEP) e.g.shift(); e.n = e.g.length; var sum = 0; for (var gi = 0; gi < e.g.length; gi++) sum += e.g[gi]; e.m = sum / e.n; var ss = 0; for (var gi = 0; gi < e.g.length; gi++) ss += (e.g[gi] - e.m) * (e.g[gi] - e.m); e.s = Math.sqrt(ss / e.n); } } function queryChains() { if (notableSeq.length < 1) return []; var candidates = {}; for (var ni = 0; ni < NGRAMS.length; ni++) { var K = NGRAMS[ni]; if (notableSeq.length < K) continue; var parts = []; for (var i = notableSeq.length - K; i < notableSeq.length; i++) parts.push(notableSeq[i].z); var key = parts.join(','); var chain = chains[key]; if (!chain || (chain._t || 0) < MIN_OBS) continue; for (var zk in chain) { if (zk === '_t') continue; var z = parseInt(zk); if (!isChainable(z)) continue; var e = chain[zk]; if (e.n < MIN_OBS) continue; var ap = getAbsentPenalty(z); if (ap === 0) continue; var kls = K * 20; var ms = Math.min(25, e.n * 5); var cv = e.m > 0 ? e.s / e.m : 1; var cs = Math.max(0, 1 - cv) * 35; var lastAge = rollCounter - notableSeq[notableSeq.length - 1].ri; var ad = Math.abs(lastAge - e.m); var gr = Math.exp(-(ad * ad) / 128); var gs = gr * 20; var score = (kls + ms + cs + gs) * ap; // v17: chaos bonus — if in chaos, boost score for post-chaos zones if (isInChaos()) { // Zones that historically follow chaos patterns get a bonus score *= 0.9; // slight penalty during chaos — less confident } var mdl = model[z]; if (mdl && mdl.ls > 0 && mdl.c > 0) { var age = rollCounter - mdl.ls; var ratio = age / mdl.c; if (ratio >= 0.7 && ratio <= 1.4) score += 8; } if (isBurst(z)) score += BURST_BOOST; var isSeed = SEED_SET[z]; if (!isSeed) score *= 0.5; if (!candidates[zk] || candidates[zk].cf < score) { candidates[zk] = { z: z, cf: score, src: key + '\u2192' + zk, n: e.n, m: e.m, s: e.s, lg: e.g.slice(-5), gr: gr, K: K, burst: isBurst(z), isSeed: isSeed, gap: e.m }; } } } var result = []; for (var k in candidates) result.push(candidates[k]); // v17.7 FIX: was b.sc - a.sc, but objects have .cf not .sc — sort was a no-op result.sort(function(a, b) { return (b.cf || 0) - (a.cf || 0); }); return result; } // ==================== MACRO CHAIN DICTIONARY ==================== function buildMacros() { if (notableSeq.length < MACRO_MIN_LEN) return; var scanLen = Math.min(notableSeq.length, MACRO_SCAN_LEN); var start = Math.max(0, notableSeq.length - scanLen); for (var pi = 0; pi < MACRO_PREFIX.length; pi++) { var PL = MACRO_PREFIX[pi]; for (var si = start; si <= notableSeq.length - MACRO_MIN_LEN; si++) { // v17: skip if prefix contains non-chainable zones (noise reduction) var allChainable = true; for (var p = 0; p < PL; p++) { if (!isChainable(notableSeq[si + p].z)) { allChainable = false; break; } } if (!allChainable) continue; var prefix = []; for (var p = 0; p < PL; p++) prefix.push(notableSeq[si + p].z); var cont = []; var gaps = []; for (var ci = si + PL; ci < notableSeq.length && ci - si < MACRO_MAX_LEN; ci++) { gaps.push(notableSeq[ci].ri - notableSeq[ci - 1].ri); cont.push(notableSeq[ci].z); } if (cont.length < 1) continue; var key = prefix.join(','); if (!macros[key]) macros[key] = []; var contStr = cont.join(','); var found = false; for (var j = 0; j < macros[key].length; j++) { if (macros[key][j].contStr === contStr) { macros[key][j].count++; macros[key][j].last = rollCounter; for (var gi = 0; gi < gaps.length; gi++) { macros[key][j].gaps.push(gaps[gi]); if (macros[key][j].gaps.length > GAP_KEEP * 3) macros[key][j].gaps.shift(); } found = true; break; } } if (!found) { macros[key].push({ contStr: contStr, cont: cont.slice(), gaps: gaps.slice(), count: 1, first: notableSeq[si].ri, last: rollCounter }); } } } } function matchMacros() { if (notableSeq.length < 2) return []; var results = []; for (var pi = 0; pi < MACRO_PREFIX.length; pi++) { var PL = MACRO_PREFIX[pi]; if (notableSeq.length < PL) continue; var prefix = []; for (var p = 0; p < PL; p++) prefix.push(notableSeq[notableSeq.length - PL + p].z); var key = prefix.join(','); var entries = macros[key]; if (!entries) continue; var bestEntry = null; var bestScore = 0; for (var j = 0; j < entries.length; j++) { var e = entries[j]; if (e.count < MACRO_MIN_OBS) continue; var age = rollCounter - e.last; var recency = Math.exp(-age * age / (2 * 5000 * 5000)); var avgGap = 0; if (e.gaps.length > 0) { for (var gi = 0; gi < e.gaps.length; gi++) avgGap += e.gaps[gi]; avgGap /= e.gaps.length; } var gapVar = 0; if (e.gaps.length > 1) { for (var gi = 0; gi < e.gaps.length; gi++) gapVar += (e.gaps[gi] - avgGap) * (e.gaps[gi] - avgGap); gapVar = Math.sqrt(gapVar / e.gaps.length); } var consistency = avgGap > 0 ? Math.max(0, 1 - gapVar / avgGap) : 0; var score = e.count * 15 + recency * 20 + consistency * 30; if (score > bestScore) { bestScore = score; bestEntry = e; } } if (!bestEntry) continue; var lastAge = rollCounter - notableSeq[notableSeq.length - 1].ri; var cumulativeGap = lastAge; for (var ci = 0; ci < bestEntry.cont.length && results.length < QUEUE_SIZE; ci++) { var z = bestEntry.cont[ci]; var dup = false; for (var ri = 0; ri < results.length; ri++) { if (results[ri].z === z) { dup = true; break; } } if (dup) continue; var ap = getAbsentPenalty(z); if (ap === 0) continue; var isSeed = SEED_SET[z]; if (ci < bestEntry.gaps.length) { cumulativeGap += bestEntry.gaps[ci]; } else { cumulativeGap += avgGap || 8; } var posBonus = Math.max(0, 40 - ci * 10); var cf = Math.min(90, posBonus + bestEntry.count * 8 + bestScore * 0.3); cf *= ap; if (!isSeed) cf *= 0.5; // v17: chaos context — if matching a CHAOS: prefix macro, // slightly boost confidence since we're in post-chaos recovery if (key.indexOf('CHAOS:') === 0) { cf = Math.min(90, cf * 1.15); } results.push({ z: z, cf: cf, src: 'M:' + key + '\u2192' + bestEntry.cont.slice(0, ci + 1).join(','), eta: cumulativeGap, gap: ci < bestEntry.gaps.length ? bestEntry.gaps[ci] : (avgGap || 8), macro: true, pos: ci, isSeed: isSeed }); } } // v17.12: User-defined chain block removed (module deleted). // v17: Also match CHAOS: prefix macros when in/recently in chaos if (isInChaos() && notableSeq.length >= 2) { // Build prefix from last 2 notable zones before boundary cluster var cprefix = 'CHAOS:'; var pStart = Math.max(0, notableSeq.length - 3); var pp = []; for (var i = pStart; i < notableSeq.length; i++) { if (!CHAOS_BOUNDARY_SET[notableSeq[i].z]) { pp.push(notableSeq[i].z); } if (pp.length >= 2) break; } if (pp.length >= 2) { cprefix += pp.join(','); var chaosEntries = macros[cprefix]; if (chaosEntries) { for (var j = 0; j < chaosEntries.length; j++) { var ce = chaosEntries[j]; if (ce.count < 2) continue; // Add first continuation as candidate if (ce.cont.length > 0) { var cz = ce.cont[0]; var dup = false; for (var ri = 0; ri < results.length; ri++) { if (results[ri].z === cz) { dup = true; break; } } if (!dup && isChainable(cz)) { var ap2 = getAbsentPenalty(cz); if (ap2 > 0) { var avgGap2 = 0; if (ce.gaps.length > 0) { for (var gi = 0; gi < ce.gaps.length; gi++) avgGap2 += ce.gaps[gi]; avgGap2 /= ce.gaps.length; } results.push({ z: cz, cf: Math.min(85, 30 + ce.count * 10) * ap2, src: 'M:' + cprefix + '\u2192' + ce.cont[0], eta: avgGap2 || 10, gap: avgGap2 || 10, macro: true, pos: 0, isSeed: SEED_SET[cz] }); } } } } } } } return results; } // ==================== OVERDELAYED GHOST TRACKING (NEW v17) ==================== function createGhost(entry) { if (ghosts.length >= MAX_GHOSTS) { // Remove oldest ghost ghosts.shift(); } ghosts.push({ z: entry.z, cf: entry.cf, src: entry.src, at: entry.at, expiredAt: rollCounter, gap: entry.gap || 10, isSeed: !!entry.isSeed, tracked: false }); } function checkGhosts(zone) { if (zone === null || ghosts.length === 0) return; for (var i = ghosts.length - 1; i >= 0; i--) { var g = ghosts[i]; var age = rollCounter - g.expiredAt; if (age > OVERDELAYED_WINDOW) { // Ghost expired — remove ghosts.splice(i, 1); continue; } if (zone === g.z && !g.tracked) { // OVERDELAYED HIT! g.tracked = true; g.hitAt = rollCounter; g.totalDelay = rollCounter - g.at; // Apply bias feedback (learn from this delayed hit) updateBias(g.z, g.gap, g.totalDelay); // Log as OVERDELAYED in prediction log plog.push({ id: plog.length + 1, at: g.at, z: g.z, cf: g.cf, src: g.src + ' [OVERDELAYED]', lf: '?', // lifecycle at original prediction time unknown mw: 0, // already expired macro: false, burst: false, queue: [g.z], done: true, rt: rollCounter, res: 'OVERDELAYED', rtr: g.totalDelay, delayed: true, overdelayed: true, nearHit: false, ghostAge: age }); // Remove ghost after logging ghosts.splice(i, 1); } } } // ==================== PREDICTION QUEUE ENGINE (v17) ==================== function isZoneInCooldown(zone) { if (zone === null || zone === undefined) return false; var ts = predCooldown[zone]; if (ts === undefined) return false; var age = rollCounter - ts; if (age >= PRED_COOLDOWN_ROLLS) { // Cooldown expired — clean up delete predCooldown[zone]; return false; } return true; } function setZoneCooldown(zone) { if (zone === null || zone === undefined) return; predCooldown[zone] = rollCounter; // Trim map: keep only zones with active cooldown var keys = Object.keys(predCooldown); for (var i = 0; i < keys.length; i++) { var z = parseInt(keys[i]); if (rollCounter - predCooldown[z] >= PRED_COOLDOWN_ROLLS) { delete predCooldown[z]; } } } function clearZoneCooldown(zone) { if (zone === null || zone === undefined) return; if (predCooldown[zone] !== undefined) delete predCooldown[zone]; } function buildQueue() { if (lifecycle === 'CHAOS') { if (burstZones) { var bestB = null, bestC = 0; for (var bz in burstZones) { if (burstZones[bz].count > bestC) { bestC = burstZones[bz].count; bestB = parseInt(bz); } } if (bestB !== null && bestC >= BURST_MIN + 1) { var bcf = Math.min(80, 35 + (bestC - BURST_MIN) * 12); return [{z: bestB, cf: bcf, src: 'BURST(' + bestC + 'x)', eta: 5, gap: 5, mw: 15, macro: false, burst: true, isSeed: SEED_SET[bestB]}]; } } return []; } // 1. Get candidates from both sources var macroCands = matchMacros(); var chainCands = queryChains(); // 2. Merge all candidates var allCands = []; for (var i = 0; i < macroCands.length; i++) allCands.push(macroCands[i]); for (var i = 0; i < chainCands.length; i++) allCands.push(chainCands[i]); // Apply lifecycle multiplier for (var i = 0; i < allCands.length; i++) { var lm = lifecycle === 'ENTER' ? 0.65 : lifecycle === 'ACTIVE' ? 1.0 : 0.35; if (allCands[i].burst) lm = Math.max(lm, 0.7); allCands[i].cf = Math.min(95, allCands[i].cf * lm); } // 3. Deduplicate by zone, keep highest confidence // v17.7: skip zones in cooldown (recently expired without hit) // v17.10: also track the skipped CANDIDATE OBJECTS (not just zone numbers) // so the stuck-loop guard below can fall back to them as alternatives. var bestPerZone = {}; var cooldownSkipped = []; // zone numbers (for display/logging) var cooldownSkippedCands = {}; // zone → best candidate object (for fallback) for (var i = 0; i < allCands.length; i++) { var c = allCands[i]; var effectiveMinCf = c.isSeed ? PRD_MIN_CF : PRD_MIN_CF * NON_SEED_CF_MULT; if (c.cf < effectiveMinCf) continue; // v17.7: cooldown gate if (isZoneInCooldown(c.z)) { if (cooldownSkipped.indexOf(c.z) === -1) cooldownSkipped.push(c.z); // v17.10: keep the best candidate object per skipped zone if (!cooldownSkippedCands[c.z] || cooldownSkippedCands[c.z].cf < c.cf) { cooldownSkippedCands[c.z] = c; } continue; } if (!bestPerZone[c.z] || bestPerZone[c.z].cf < c.cf) { bestPerZone[c.z] = c; } } // 4. Sort by ETA (soonest first) var sorted = []; for (var zk in bestPerZone) sorted.push(bestPerZone[zk]); sorted.sort(function(a, b) { return (a.eta || 999) - (b.eta || 999); }); // v17.10: Track ALL candidates that were filtered out by cooldown, so we can // fall back to them if the final queue has only one entry and it's the same // zone as the just-expired prediction (stuck-loop guard). // 5. Build queue var queue = []; var lastAge = rollCounter - (notableSeq.length > 0 ? notableSeq[notableSeq.length - 1].ri : 0); for (var i = 0; i < sorted.length && queue.length < QUEUE_SIZE; i++) { var c = sorted[i]; var rawEta = Math.max(1, c.gap - lastAge); var adjEta = Math.max(1, Math.round(applyBias(c.z, rawEta))); var mw = calcMaxWait(c.gap || 8); // v17: if currently in chaos, extend mw if (isInChaos()) { mw += CHAOS_DELAY_BONUS; mw = Math.min(PRD_MAX_WAIT + CHAOS_DELAY_BONUS, mw); } queue.push({ z: c.z, cf: c.cf, src: c.src, eta: adjEta, gap: c.gap, mw: mw, macro: !!c.macro, burst: !!c.burst, isSeed: !!c.isSeed, at: rollCounter, status: queue.length === 0 ? 'active' : 'queued', delayed: false, delayStart: 0, nearHit: false, chaosExtended: false // v17: track if chaos already extended this }); } // v17.10 STUCK-LOOP GUARD: if (queue.length === 1 && queue[0].z !== undefined) { var altBest = null; // First try cooldown-skipped candidates (the most likely source of alternatives) for (var altZ in cooldownSkippedCands) { if (parseInt(altZ) === queue[0].z) continue; var altC = cooldownSkippedCands[altZ]; if (!altBest || altC.cf > altBest.cf) altBest = altC; } if (altBest) { var altRawEta = Math.max(1, altBest.gap - lastAge); var altAdjEta = Math.max(1, Math.round(applyBias(altBest.z, altRawEta))); var altMw = calcMaxWait(altBest.gap || 8); if (isInChaos()) { altMw += CHAOS_DELAY_BONUS; altMw = Math.min(PRD_MAX_WAIT + CHAOS_DELAY_BONUS, altMw); } queue.push({ z: altBest.z, cf: Math.max(1, Math.round(altBest.cf)), src: altBest.src + ' [ALT]', eta: altAdjEta, gap: altBest.gap, mw: altMw, macro: !!altBest.macro, burst: !!altBest.burst, isSeed: !!altBest.isSeed, at: rollCounter, status: 'queued', delayed: false, delayStart: 0, nearHit: false, chaosExtended: false }); } } return queue; } function calcMaxWait(gapMean) { var mw = Math.round(gapMean * PRD_WAIT_MULT); return Math.max(PRD_MIN_WAIT, Math.min(PRD_MAX_WAIT, mw)); } function updateQueue() { var needRebuild = false; if (predQueue.length === 0) { needRebuild = true; } else { var active = predQueue[0]; if (!active) { needRebuild = true; } else if (active.status === 'resolved') { predQueue.shift(); if (predQueue.length > 0 && predQueue[0].status === 'queued') { predQueue[0].status = 'active'; predQueue[0].at = rollCounter; } if (predQueue.length === 0) needRebuild = true; } else if (active.status === 'active') { var age = rollCounter - active.at; if (age > active.mw && !active.delayed) { active.delayed = true; active.delayStart = rollCounter; } if (active.delayed && rollCounter - active.delayStart > DELAYED_EXT) { // FEEDBACK — learn from this failure updateBias(active.z, active.gap || 10, rollCounter - active.at); // v17.2: Update drift on expiry too updateDrift({z: active.z, rtr: rollCounter - active.at, mw: active.mw}); var res = active.nearHit ? 'NEAR' : 'EXPIRED'; // v17: CREATE GHOST before resolving createGhost(active); // v17.7: Set cooldown on the expired zone so it doesn't get // re-predicted immediately. NEAR counts as a soft failure // (close but no hit) so it also enters cooldown. HIT/DELAYED // do NOT enter cooldown (handled in resolveQueueEntry/checkQueueHit). if (res === 'EXPIRED' || res === 'NEAR') { setZoneCooldown(active.z); } resolveQueueEntry(0, res); needRebuild = true; } // Last resort: in-place confidence update if (!needRebuild && !active.delayed && age > active.mw * 0.8 && !active.nearHit) { if (rollCounter - lastRebuild > NO_REBUILD_PERIOD) { var betterCf = findBestCfForZone(active.z); if (betterCf > active.cf + REPLACE_CF_BOOST) { active.cf = betterCf; lastRebuild = rollCounter; } } } } } if (needRebuild) { var newQueue = buildQueue(); if (newQueue.length > 0) { if (predQueue.length > 0) { var cur = predQueue[0]; if (cur.status === 'active') { var curAge = rollCounter - cur.at; if (curAge < cur.mw * IMMUNITY_FRAC) { for (var i = 0; i < newQueue.length; i++) { if (newQueue[i].z === cur.z) { if (newQueue[i].cf > cur.cf + 5) { cur.cf = Math.round((cur.cf + newQueue[i].cf) / 2); } return; } } return; // immune — keep current } } } for (var i = 0; i < predQueue.length; i++) { if (predQueue[i].status === 'active') { resolveQueueEntry(i, 'REPLACED'); } } var entry = newQueue[0]; plog.push({ id: plog.length + 1, at: rollCounter, z: entry.z, cf: entry.cf, src: entry.src, lf: lifecycle, mw: entry.mw, macro: !!entry.macro, burst: !!entry.burst, queue: newQueue.map(function(q) { return q.z; }), done: false }); predQueue = newQueue; lastRebuild = rollCounter; } } } function findBestCfForZone(zone) { var chainCands = queryChains(); var best = 0; for (var i = 0; i < chainCands.length; i++) { if (chainCands[i].z === zone && chainCands[i].cf > best) { best = chainCands[i].cf; } } var macroCands = matchMacros(); for (var i = 0; i < macroCands.length; i++) { if (macroCands[i].z === zone && macroCands[i].cf > best) { best = macroCands[i].cf; } } return best; } function resolveQueueEntry(idx, result) { if (idx >= predQueue.length) return; var entry = predQueue[idx]; if (entry.status !== 'active') return; entry.status = 'resolved'; entry.res = result; entry.rt = rollCounter; entry.rtr = rollCounter - entry.at; // DELAY FEEDBACK on resolution if (result === 'HIT' || result === 'DELAYED') { updateBias(entry.z, entry.gap || 10, entry.rtr); // v17.7: clear cooldown on successful hit — the zone proved it can hit if (PRED_COOLDOWN_HIT_CLEAR) clearZoneCooldown(entry.z); } // v17.2: Update drift corrector on ANY resolution updateDrift(entry); // Update prediction log for (var i = plog.length - 1; i >= 0; i--) { var pl = plog[i]; if (!pl.done && pl.z === entry.z && pl.at === entry.at) { pl.done = true; pl.rt = rollCounter; pl.res = result; pl.rtr = rollCounter - pl.at; pl.delayed = !!entry.delayed; pl.nearHit = !!entry.nearHit; if (entry.nearZone !== undefined) pl.nearZone = entry.nearZone; break; } } } function checkQueueHit(zone) { if (zone === null || predQueue.length === 0) return; for (var i = 0; i < predQueue.length; i++) { var q = predQueue[i]; if (q.status !== 'active' && q.status !== 'queued') continue; // Exact hit if (zone === q.z) { if (i === 0) { var result = q.delayed ? 'DELAYED' : 'HIT'; resolveQueueEntry(i, result); predQueue.shift(); if (predQueue.length > 0 && predQueue[0].status === 'queued') { predQueue[0].status = 'active'; predQueue[0].at = rollCounter; } if (predQueue.length === 0) { var nq = buildQueue(); if (nq.length > 0) { plog.push({ id: plog.length + 1, at: rollCounter, z: nq[0].z, cf: nq[0].cf, src: nq[0].src, lf: lifecycle, mw: nq[0].mw, macro: !!nq[0].macro, burst: !!nq[0].burst, queue: nq.map(function(q) { return q.z; }), done: false }); predQueue = nq; lastRebuild = rollCounter; } } } else { q.status = 'resolved'; q.res = 'HIT'; q.rt = rollCounter; q.rtr = rollCounter - (q.at || rollCounter); } return; } // Near hit (active entry only) if (i === 0 && Math.abs(zone - q.z) <= ZONE_SPREAD && zone !== q.z) { if (!q.nearHit) { q.nearHit = true; q.nearZone = zone; if (!q.delayed) { q.mw = Math.max(q.mw, rollCounter - q.at + 15); } } } } } // ==================== REGISTER HIT ==================== function registerHit(zone) { if (zone !== null) { var m = model[zone]; var gap = rollCounter - m.ls; if (m.ls > 0 && gap > 0) m.c = m.c * 0.7 + gap * 0.3; m.h++; m.ls = rollCounter; } if (rollCounter % 50 === 0) dynZones = computeDynZones(); updateBurstZones(zone); updateLifecycle(); // v17: detect chaos BEFORE checking queue detectChaos(zone); checkQueueHit(zone); // v17: check ghosts AFTER queue (ghosts are passive observers) checkGhosts(zone); if (isChainable(zone)) { addChainEntries(zone); notableSeq.push({ z: zone, ri: rollCounter }); if (notableSeq.length > MAX_NOTABLE) notableSeq = notableSeq.slice(-MAX_NOTABLE); buildMacros(); } updateQueue(); } // ==================== OVERLAY ==================== function createOverlay() { overlay = document.createElement('div'); overlay.id = 'dice-analyzer-overlay'; overlay.style.cssText = 'position:fixed;top:40px;right:20px;width:350px;max-height:94vh;' + 'overflow:auto;z-index:999999;background:#0a0a0a;color:#00ff00;' + 'font-family:monospace;font-size:12px;line-height:1.2;' + 'border:2px solid #00ff00;padding:6px 8px;cursor:move;' + 'box-shadow:0 0 15px rgba(0,255,0,0.1);'; document.body.appendChild(overlay); dragElement(overlay); } function dragElement(el) { var x = 0, y = 0; el.onmousedown = function(e) { x = e.clientX; y = e.clientY; document.onmouseup = stop; document.onmousemove = move; }; function move(e) { el.style.top = (el.offsetTop - (y - e.clientY)) + 'px'; el.style.left = (el.offsetLeft - (x - e.clientX)) + 'px'; x = e.clientX; y = e.clientY; } function stop() { document.onmouseup = null; document.onmousemove = null; } } // ==================== RENDER ==================== // v17: safe toFixed helper function sfmt(v, d) { var n = parseFloat(v); if (isNaN(n)) return d !== undefined ? d : '0'; return n.toFixed(d === undefined ? 0 : d); } function render() { var html = ''; var seedRate = getSeedRate(); // Header html += '