EP Answer Helper + Anti-Snitch + Maths Solver + MCQ + Gaps + Auto-Skip
// ==UserScript==
// @name EP Hacks
// @namespace http://tampermonkey.net/
// @version 7.0
// @description EP Answer Helper + Anti-Snitch + Maths Solver + MCQ + Gaps + Auto-Skip
// @match https://app.educationperfect.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
"use strict";
// ── EXAM BYPASS (silent, always on) ───────────────────────────
document.addEventListener('contextmenu', e => e.stopImmediatePropagation(), true);
document.addEventListener('selectstart', e => e.stopImmediatePropagation(), true);
document.addEventListener('copy', e => e.stopImmediatePropagation(), true);
// ── ANTI-SNITCH ───────────────────────────────────────────────
let antiSnitchActive = true;
function stopSnitch(e) {
if (antiSnitchActive) e.stopImmediatePropagation();
}
// Spoof visibility and focus APIs
function applyAntiSnitchSpoof() {
try {
Object.defineProperty(document, 'visibilityState', { get: () => antiSnitchActive ? 'visible' : 'hidden', configurable: true });
Object.defineProperty(document, 'hidden', { get: () => antiSnitchActive ? false : true, configurable: true });
// Fix 9: patch hasFocus too
const origHasFocus = document.hasFocus.bind(document);
document.hasFocus = () => antiSnitchActive ? true : origHasFocus();
} catch(e) {}
}
applyAntiSnitchSpoof();
// Register all event interceptors once
document.addEventListener('visibilitychange', stopSnitch, true);
window.addEventListener('blur', stopSnitch, true);
window.addEventListener('focus', stopSnitch, true);
window.addEventListener('pagehide', stopSnitch, true);
document.addEventListener('fullscreenchange', stopSnitch, true);
document.addEventListener('webkitfullscreenchange', stopSnitch, true);
// Fake mouse movement to keep activity tracker happy
setInterval(() => {
if (!antiSnitchActive) return;
document.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
clientX: 200 + Math.random() * 400,
clientY: 200 + Math.random() * 300,
}));
}, 3000);
// ── AUTO-SKIP ─────────────────────────────────────────────────
let autoSkipActive = false;
// Fix 11: broader info slide detection
setInterval(() => {
try {
if (!autoSkipActive) return;
// Try specific selector first, then broader fallback
const infoSlide =
document.querySelector('.h-group.v-align-center.expanded-content.information.selected') ||
document.querySelector('[class*="information"][class*="selected"]') ||
document.querySelector('[class*="info-slide"][class*="active"]');
if (!infoSlide) return;
document.querySelectorAll('.continue.arrow.action-bar-button.v-group.ng-isolate-scope button, [class*="continue"][class*="action"] button').forEach(btn => btn.click());
} catch(e) {}
}, 100);
// ── HELPERS ───────────────────────────────────────────────────
// Fix 1: Angular fallback + Fix 14: broader scope search
function getScope() {
try {
if (!window.angular) {
console.warn('[EP Hacks] Angular not found');
return null;
}
const selectors = ['.lp-answer-input', '[class*="answer-input"]', '[class*="lp-answer"]'];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) {
const scope = angular.element(el).scope();
if (scope?.self?.model) return scope;
}
}
// Fallback to answer-text input
const input = [...document.querySelectorAll('#answer-text, [id*="answer"]')].find(el => el.tagName === 'INPUT');
if (input) return angular.element(input).scope();
return null;
} catch(e) { console.warn('[EP Hacks] getScope error:', e); return null; }
}
function getQuestion() {
try {
const scope = getScope();
return scope?.self?.model?._currentQuestion || null;
} catch(e) { return null; }
}
function parseArr(expr) {
const match = expr?.match(/\[([^\]]+)\]/);
if (!match) return null;
return match[1].split(',').map(s => s.replace(/['"]/g, '').trim());
}
function resolveVars(vars, rng) {
const resolved = { rng };
vars?.forEach(v => {
if (v.Name === 'rng') return;
if (v.value !== null && v.value !== undefined && !v.expression) {
resolved[v.Name] = String(v.value);
} else if (v.expression?.includes('[rng]')) {
const arr = parseArr(v.expression);
if (arr) resolved[v.Name] = arr[rng];
}
});
return resolved;
}
function needsDegreeSymbol() {
try {
const pageText = document.body?.innerText || '';
return /degree symbol|include.*°|°.*symbol/i.test(pageText);
} catch(e) { return false; }
}
// ── ANSWER GETTERS ────────────────────────────────────────────
// 1. Standard language answer
function getStandardAnswer() {
try {
const q = getQuestion();
if (!q) return null;
const answer = q.validAnswers?.[0]?.outputString;
if (!answer) return null;
return { answer, prompt: q.specifiedDisplayName || null, type: 'standard' };
} catch(e) { console.warn('[EP Hacks] Standard answer error:', e); return null; }
}
// 2. MCQ answer
function getMCQAnswer() {
try {
const q = getQuestion();
if (!q || q.questionType !== 6) return null;
const components = q.questionDef?.Components;
if (!components?.length) return null;
if (!components[0]?.Options?.some(o => o.Correct === 'true' || o.Correct === true)) return null;
// Fix 15: check visible buttons not just presence
const visibleBtns = [...document.querySelectorAll('.mcq-preview-option')].filter(b => b.offsetParent !== null);
if (!visibleBtns.length) return null;
const vars = q.questionGeneratedVariables;
const rng = vars?.find(v => v.Name === 'rng')?.value || 0;
const resolved = resolveVars(vars, rng);
const correct = components[0].Options.find(o => o.Correct === 'true' || o.Correct === true);
if (!correct) return null;
const tmpl = correct.TextTemplate;
// Substitute \var{name} with resolved values
const tmplResolved = tmpl.replace(/(?:\\\\|\\)?var\{(\w+)\}/g, (_, k) => resolved[k] !== undefined ? resolved[k] : k);
const trigFn = tmpl.includes('sin') ? 'sin' : tmpl.includes('cos') ? 'cos' : tmpl.includes('tan') ? 'tan' : null;
// Extract frac parts from resolved template
const fracParts = tmplResolved.match(/frac\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/);
const numPart = fracParts?.[1] || '';
const denPart = fracParts?.[2] || '';
const numPartHasVar = !/(sin|cos|tan)/.test(numPart);
const numVarVal = numPart.replace(/[^0-9.]/g, '') || null;
const denVarVal = denPart.replace(/[^0-9.]/g, '') || null;
// Simple {x}/{y} pattern
const simpleFrac = tmpl.match(/\{(\w+)\}\/\{(\w+)\}/);
const numVal = simpleFrac ? resolved[simpleFrac[1]] : null;
const denVal = simpleFrac ? resolved[simpleFrac[2]] : null;
// Scope to active container
const activeContainer = document.querySelector('.multi-choice-component');
const buttons = activeContainer
? [...activeContainer.querySelectorAll('.mcq-preview-option')]
: visibleBtns;
// Clear all borders
document.querySelectorAll('.mcq-preview-option').forEach(btn => {
btn.style.outline = '';
btn.style.borderRadius = '';
});
let bestBtn = null;
let bestScore = -1;
buttons.forEach(btn => {
const text = btn.textContent.toLowerCase().replace(/\s/g, '');
let score = 0;
// Trig function match
if (trigFn) {
if (text.includes(trigFn)) score += 5;
else score -= 10;
}
// Number position relative to trig function
if (trigFn && (numVarVal || denVarVal)) {
const numericVal = String(numVarVal || denVarVal);
const fnIdx = text.indexOf(trigFn);
const numIdx = text.indexOf(numericVal);
if (fnIdx !== -1 && numIdx !== -1) {
if (numPartHasVar && numIdx < fnIdx) score += 6;
else if (numPartHasVar && numIdx > fnIdx) score -= 6;
else if (!numPartHasVar && numIdx > fnIdx) score += 6;
else if (!numPartHasVar && numIdx < fnIdx) score -= 6;
}
}
// Simple opp/hyp order
if (!numVarVal && numVal && denVal) {
const ni = text.indexOf(String(numVal));
const di = text.indexOf(String(denVal));
if (ni !== -1 && di !== -1 && ni < di) score += 5;
}
// Text match — substitute vars and compare
{
let resolvedTmpl = tmplResolved.replace(/`/g, '').replace(/['"]/g, '').toLowerCase();
Object.keys(resolved).forEach(k => {
resolvedTmpl = resolvedTmpl.split('{' + k + '}').join(String(resolved[k]));
});
const cleanTmpl = resolvedTmpl.replace(/\\[a-zA-Z]+/g, '').replace(/[^a-z0-9]/g, '');
const cleanText = text.replace(/[^a-z0-9]/g, '');
if (cleanTmpl.length > 2 && cleanText.includes(cleanTmpl)) score += 8;
}
if (score > bestScore) { bestScore = score; bestBtn = btn; }
});
if (bestBtn && bestScore > 0) {
bestBtn.style.outline = '3px solid #3fb950';
bestBtn.style.borderRadius = '6px';
}
const displayPrompt = tmplResolved.replace(/`/g, '').replace(/['"]/g, '').replace(/\\[a-zA-Z]+/g, '').trim().slice(0, 40);
return { answer: 'Select option with green border', prompt: displayPrompt, type: 'mcq' };
} catch(e) { console.warn('[EP Hacks] MCQ error:', e); return null; }
}
// 3. Fill in the gaps
function getFillGapsAnswer() {
try {
const q = getQuestion();
if (!q) return null;
const comp = q.questionDef?.Components?.find(c => c.ComponentTypeCode === 'FILL_IN_GAPS_COMPONENT');
if (!comp?.Gaps?.length) return null;
const answers = comp.Gaps.map((gap, i) => `Box ${i + 1}: ${gap.CorrectOptions?.[0] || '?'}`);
return { answer: answers.join(' | '), prompt: `Fill in ${comp.Gaps.length} gap${comp.Gaps.length > 1 ? 's' : ''}`, type: 'gaps' };
} catch(e) { console.warn('[EP Hacks] Gaps error:', e); return null; }
}
// 4. Maths (generated variables)
function getMathsAnswer() {
try {
const q = getQuestion();
if (!q || q.questionType !== 6) return null;
const vars = q.questionGeneratedVariables;
if (!vars?.length) return null;
// Skip if visible MCQ buttons on screen
const visibleBtns = [...document.querySelectorAll('.mcq-preview-option')].filter(b => b.offsetParent !== null);
if (visibleBtns.length) return null;
const rng = vars.find(v => v.Name === 'rng')?.value || 0;
const resolved = {};
vars.forEach(v => {
if (v.Name === 'rng') { resolved.rng = rng; return; }
if (v.value !== null && v.value !== undefined && !v.expression) {
resolved[v.Name] = v.value;
} else if (v.expression?.includes('[rng]')) {
const arr = parseArr(v.expression);
if (arr) resolved[v.Name] = parseFloat(arr[rng]);
}
});
const answerVar = vars.find(v => ['answer', 'ans', 'rans'].includes(v.Name));
if (!answerVar?.expression) return null;
// Fix 7: global fix() replacement
let expr = answerVar.expression
.replace(/fix\(([^,]+),(\d+)\)/g, 'parseFloat(($1).toFixed($2))')
.replace(/\bpi\b/g, 'Math.PI')
.replace(/\bcos\b/g, 'Math.cos')
.replace(/\bsin\b/g, 'Math.sin')
.replace(/\btan\b/g, 'Math.tan')
.replace(/\bsqrt\b/g, 'Math.sqrt')
.replace(/\babs\b/g, 'Math.abs')
.replace(/\bround\b/g, 'Math.round')
.replace(/(?<!Math\.)asin\b/g, 'Math.asin')
.replace(/(?<!Math\.)acos\b/g, 'Math.acos')
.replace(/(?<!Math\.)atan\b/g, 'Math.atan');
// Fix 5: word boundary replacement
Object.keys(resolved).sort((a, b) => b.length - a.length).forEach(k => {
expr = expr.replace(new RegExp('\\b' + k + '\\b', 'g'), String(resolved[k]));
});
// Fix 6: debug eval errors
let answer;
try {
answer = eval(expr);
} catch(e) {
console.warn('[EP Hacks] Maths eval error:', expr, e);
return null;
}
if (isNaN(answer) || !isFinite(answer)) return null;
const deg = needsDegreeSymbol() ? '°' : '';
return { answer: String(answer) + deg, prompt: `🧮 ${answerVar.expression}`, type: 'maths' };
} catch(e) { console.warn('[EP Hacks] Maths error:', e); return null; }
}
function getAnswer() {
return getStandardAnswer() || getMCQAnswer() || getFillGapsAnswer() || getMathsAnswer();
}
// ── BUILD UI ──────────────────────────────────────────────────
function buildUI() {
document.getElementById('ep-hacks-panel')?.remove();
document.getElementById('ep-hacks-reopen')?.remove();
const reopenBtn = document.createElement('div');
reopenBtn.id = 'ep-hacks-reopen';
reopenBtn.style.cssText = `
position:fixed;top:16px;right:16px;z-index:999999;
width:32px;height:32px;border-radius:50%;
background:#58a6ff;color:#0f1117;
font-size:18px;font-weight:700;
display:none;align-items:center;justify-content:center;
cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.4);
`;
reopenBtn.textContent = '⚡';
document.body.appendChild(reopenBtn);
const panel = document.createElement('div');
panel.id = 'ep-hacks-panel';
panel.style.cssText = `
position:fixed;top:16px;right:16px;z-index:999999;
background:#0f1117;color:#e2e8f0;
border:1px solid #30363d;border-radius:12px;
padding:16px 20px;font-family:'Fira Code',monospace;
font-size:13px;min-width:240px;
box-shadow:0 8px 32px rgba(0,0,0,0.6);
`;
panel.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
<span style="font-size:14px;font-weight:700;color:#58a6ff;">⚡ EP Hacks</span>
<button id="ep-hacks-close" style="background:none;border:none;color:#8b949e;cursor:pointer;font-size:18px;padding:0;line-height:1;">×</button>
</div>
<div style="margin-bottom:14px;">
<div style="font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;">Answer</div>
<div id="ep-hacks-prompt" style="font-size:11px;color:#484f58;margin-bottom:4px;">—</div>
<div id="ep-hacks-answer" style="font-size:22px;font-weight:700;color:#3fb950;min-height:28px;letter-spacing:0.02em;cursor:pointer;" title="Click to copy">—</div>
<div id="ep-hacks-type" style="font-size:10px;color:#484f58;margin-top:4px;"></div>
</div>
<div style="height:1px;background:#21262d;margin-bottom:14px;"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div>
<div style="font-size:12px;font-weight:600;color:#e2e8f0;">Anti-Snitch</div>
<div style="font-size:10px;color:#8b949e;">Hides tab switching</div>
</div>
<div id="ep-snitch-toggle" style="width:40px;height:22px;border-radius:11px;background:#238636;cursor:pointer;position:relative;transition:background 0.2s;">
<div id="ep-snitch-knob" style="position:absolute;top:3px;left:21px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left 0.2s;"></div>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div>
<div style="font-size:12px;font-weight:600;color:#e2e8f0;">Auto-Skip Info</div>
<div style="font-size:10px;color:#8b949e;">Skips learning slides</div>
</div>
<div id="ep-skip-toggle" style="width:40px;height:22px;border-radius:11px;background:#484f58;cursor:pointer;position:relative;transition:background 0.2s;">
<div id="ep-skip-knob" style="position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left 0.2s;"></div>
</div>
</div>
<div style="height:1px;background:#21262d;margin-bottom:10px;"></div>
<button id="ep-exit-fullscreen" style="
width:100%;padding:7px;background:#21262d;color:#8b949e;
border:1px solid #30363d;border-radius:6px;cursor:pointer;
font-size:11px;font-family:'Fira Code',monospace;text-align:left;">
⛶ Enter Fullscreen
</button>
`;
document.body.appendChild(panel);
// Close / reopen
document.getElementById('ep-hacks-close').addEventListener('click', () => {
panel.style.display = 'none';
reopenBtn.style.display = 'flex';
});
reopenBtn.addEventListener('click', () => {
panel.style.display = '';
reopenBtn.style.display = 'none';
});
// Anti-snitch toggle
document.getElementById('ep-snitch-toggle').addEventListener('click', () => {
antiSnitchActive = !antiSnitchActive;
document.getElementById('ep-snitch-toggle').style.background = antiSnitchActive ? '#238636' : '#484f58';
document.getElementById('ep-snitch-knob').style.left = antiSnitchActive ? '21px' : '3px';
});
// Auto-skip toggle
document.getElementById('ep-skip-toggle').addEventListener('click', () => {
autoSkipActive = !autoSkipActive;
document.getElementById('ep-skip-toggle').style.background = autoSkipActive ? '#238636' : '#484f58';
document.getElementById('ep-skip-knob').style.left = autoSkipActive ? '21px' : '3px';
});
// Fullscreen toggle
const fsBtn = document.getElementById('ep-exit-fullscreen');
let reallyFullscreen = false;
fsBtn.addEventListener('click', () => {
if (reallyFullscreen) {
document.exitFullscreen().catch(() => {});
reallyFullscreen = false;
fsBtn.textContent = '⛶ Enter Fullscreen';
} else {
document.documentElement.requestFullscreen().catch(() => {});
reallyFullscreen = true;
fsBtn.textContent = '⛶ Exit Fullscreen';
}
});
}
// ── ANSWER LOOP ───────────────────────────────────────────────
function startAnswerLoop() {
let lastAnswer = null;
setInterval(() => {
const data = getAnswer();
if (!data?.answer || data.answer === lastAnswer) return;
lastAnswer = data.answer;
const promptEl = document.getElementById('ep-hacks-prompt');
const answerEl = document.getElementById('ep-hacks-answer');
const typeEl = document.getElementById('ep-hacks-type');
if (promptEl) promptEl.textContent = data.prompt || '—';
if (answerEl) {
if (data.type === 'gaps') {
answerEl.innerHTML = data.answer.split(' | ')
.map(a => `<div style="font-size:15px;margin-bottom:4px;">${a}</div>`)
.join('');
} else {
answerEl.textContent = data.answer;
}
if (data.type !== 'mcq' && data.type !== 'gaps') {
navigator.clipboard.writeText(data.answer).catch(() => {});
}
answerEl.onclick = () => {
const text = data.type === 'gaps' ? data.answer.replace(/ \| /g, ', ') : data.answer;
navigator.clipboard.writeText(text).then(() => {
const orig = answerEl.style.color;
answerEl.style.color = '#58a6ff';
setTimeout(() => answerEl.style.color = orig, 500);
}).catch(() => {});
};
}
if (typeEl) typeEl.textContent =
data.type === 'maths' ? '🧮 Maths' :
data.type === 'mcq' ? '🔵 Multiple Choice' :
data.type === 'gaps' ? '✏️ Fill in Gaps' :
'📝 Language';
}, 500);
}
// ── INIT — Fix 8: MutationObserver instead of weak polling ────
let uiBuilt = false;
function tryInit() {
if (uiBuilt) return;
if (!window.angular) return;
const lpEl = document.querySelector('.lp-answer-input, [class*="lp-answer"]');
const input = [...document.querySelectorAll('#answer-text, [id*="answer-text"]')].find(el => el.tagName === 'INPUT');
if (!lpEl && !input) return;
uiBuilt = true;
buildUI();
startAnswerLoop();
}
// Fix 13: MutationObserver watches for EP's SPA navigation
const observer = new MutationObserver(() => {
tryInit();
// Rebuild UI if panel was removed by EP's re-render
if (uiBuilt && !document.getElementById('ep-hacks-panel') && !document.getElementById('ep-hacks-reopen')) {
uiBuilt = false;
tryInit();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
// Also keep a fallback interval in case MutationObserver misses early load
const initInterval = setInterval(() => {
tryInit();
if (uiBuilt) clearInterval(initInterval);
}, 300);
})();