EP Answer Helper + Anti-Snitch + Maths Solver + MCQ + Gaps + Auto-Skip
// ==UserScript==
// @name EP Hacks
// @namespace http://tampermonkey.net/
// @version 6.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();
}
try {
Object.defineProperty(document, 'visibilityState', { get: () => antiSnitchActive ? 'visible' : 'hidden', configurable: true });
Object.defineProperty(document, 'hidden', { get: () => antiSnitchActive ? false : true, configurable: true });
} catch(e) {}
document.addEventListener('visibilitychange', stopSnitch, true);
window.addEventListener('blur', stopSnitch, true);
window.addEventListener('focus', stopSnitch, true);
window.addEventListener('pagehide', stopSnitch, true);
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;
setInterval(() => {
try {
if (!autoSkipActive) return;
const infoSlide = document.querySelector('.h-group.v-align-center.expanded-content.information.selected');
if (!infoSlide) return;
document.querySelectorAll('.continue.arrow.action-bar-button.v-group.ng-isolate-scope button').forEach(btn => btn.click());
} catch(e) {}
}, 100);
// ── HELPERS ───────────────────────────────────────────────────
function getScope() {
try {
if (!window.angular) return null;
// Try lp-answer-input first (works for all question types incl. exams)
const lpEl = document.querySelector('.lp-answer-input');
if (lpEl) return angular.element(lpEl).scope();
// Fallback to answer-text input
const input = [...document.querySelectorAll('#answer-text')].find(el => el.tagName === 'INPUT');
if (input) return angular.element(input).scope();
return null;
} catch(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;
// Direct value — always resolve regardless of rng
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;
}
// Check question text for degree symbol requirement
function needsDegreeSymbol() {
try {
const text = document.querySelector('.question-text, [class*="question-body"], .lp-answer-input')?.closest('[class*="question"], [class*="game"]')?.innerText || '';
return /degree symbol|include.*°|°.*symbol/i.test(text);
} 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) { 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;
// Must have Options with Correct flag AND visible MCQ buttons on screen
if (!components[0]?.Options?.some(o => o.Correct === 'true' || o.Correct === true)) return null;
if (!document.querySelector('.mcq-preview-option')) 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');
if (!correct) return null;
const tmpl = correct.TextTemplate;
// Pre-substitute all \var{} in template for use throughout
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 numerator/denominator from resolved template
const fracParts = tmplResolved.match(/frac\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/);
const numPart = fracParts?.[1] || '';
const denPart = fracParts?.[2] || '';
// Check if numerator/denominator contains a plain number (not trig)
// After resolution, a numeric part will just be a number like "5.9"
const numPartIsNumber = /^[\d.]+$/.test(numPart.trim()) || (/^[\d.]+$/.test(numPart.replace(/[^0-9.]/g,''))) && !/(sin|cos|tan)/.test(numPart);
const numPartHasVar = !/(sin|cos|tan)/.test(numPart);
const denPartHasVar = !/(sin|cos|tan)/.test(denPart);
// Get numeric values from parts
const numVarVal = numPart.replace(/[^0-9.]/g,'') || null;
const denVarVal = denPart.replace(/[^0-9.]/g,'') || null;
// Extract simple {x}/{y} pattern
const simpleFrac = tmpl.match(/\{(\w+)\}\/\{(\w+)\}/);
const numVal = simpleFrac ? resolved[simpleFrac[1]] : null;
const denVal = simpleFrac ? resolved[simpleFrac[2]] : null;
// Highlight correct button
const activeContainer = document.querySelector('.multi-choice-component');
const buttons = activeContainer
? [...activeContainer.querySelectorAll('.mcq-preview-option')]
: [...document.querySelectorAll('.mcq-preview-option')];
// Clear all borders first
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 order — check if numeric value appears before or after 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) {
// Number should be BEFORE trig fn (numerator)
if (numIdx < fnIdx) score += 6;
else score -= 6;
} else {
// Number should be AFTER trig fn (denominator)
if (numIdx > fnIdx) score += 6;
else score -= 6;
}
}
}
// Number order for {opp}/{hyp} style
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 all vars into template and match against button
{
// Use pre-resolved template
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';
}
return {
answer: 'Select option with green border',
prompt: trigFn && numVarVal ? `${trigFn}(${numVarVal}/${denVarVal})` : tmpl.replace(/`/g,'').replace(/\\[a-zA-Z]+/g,'').slice(0,40),
type: 'mcq',
};
} catch(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) { 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 there are visible MCQ buttons (real MCQ question)
if (document.querySelector('.mcq-preview-option')) 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 => v.Name === 'answer' || v.Name === 'ans' || v.Name === 'rans');
if (!answerVar?.expression) return null;
let expr = answerVar.expression
.replace(/^fix\((.+),(\d+)\)$/, '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\.)\basin\b/g, 'Math.asin')
.replace(/(?<!Math\.)\bacos\b/g, 'Math.acos')
.replace(/(?<!Math\.)\batan\b/g, 'Math.atan');
// Sort keys longest first to avoid partial replacements
Object.keys(resolved).sort((a, b) => b.length - a.length).forEach(k => {
expr = expr.replaceAll(k, String(resolved[k]));
});
const answer = eval(expr);
if (isNaN(answer) || !isFinite(answer)) return null;
// Check if degrees symbol needed
const deg = needsDegreeSymbol() ? '°' : '';
return {
answer: String(answer) + deg,
prompt: `🧮 ${answerVar.expression}`,
type: 'maths',
};
} catch(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;">
<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>
`;
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
let snitchOn = true;
document.getElementById('ep-snitch-toggle').addEventListener('click', () => {
snitchOn = !snitchOn;
antiSnitchActive = snitchOn;
document.getElementById('ep-snitch-toggle').style.background = snitchOn ? '#238636' : '#484f58';
document.getElementById('ep-snitch-knob').style.left = snitchOn ? '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';
});
}
// ── 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;
}
// Auto-copy for non-MCQ, non-gaps
if (data.type !== 'mcq' && data.type !== 'gaps') {
navigator.clipboard.writeText(data.answer).catch(() => {});
}
// Click to copy
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 ──────────────────────────────────────────────────────
// Wait for Angular to be ready
const init = setInterval(() => {
if (!window.angular) return;
const lpEl = document.querySelector('.lp-answer-input');
const input = [...document.querySelectorAll('#answer-text')].find(el => el.tagName === 'INPUT');
if (!lpEl && !input) return;
clearInterval(init);
buildUI();
startAnswerLoop();
}, 300);
})();