'use strict';
/* ═══════════════════════════════════════════════════════════════
Интерактив «Файлы и маски» — 10 класс
Три уровня: простой, средний, ЕГЭ
═══════════════════════════════════════════════════════════════ */
// ── База имён файлов ─────────────────────────────────────────
const FILES = [
// Документы
'отчёт.docx','отчёт1.docx','отчёт2.docx','отчёт_финал.docx','отчёт_2024.docx','отчёт_2025.docx',
'Отчёт_важный.docx','ОТЧЁТ_ИТОГ.docx','план.docx','план_работы.docx','план2025.docx','план_2024.docx',
'документ.doc','документ1.doc','документ2.doc','документ.docx','документ.pdf','sample.doc','пример.docx',
'отчёт.pdf','отчёт_final.pdf',
// Фото
'фото1.jpg','фото2.jpg','фото3.jpg','фото10.jpg','фото_лето.jpg','фото_зима.jpg','Фото_море.jpg','ФОТО_ГОРЫ.jpg',
'family.jpg','holiday.jpg','screen.jpg',
// PNG
'cat.png','dog.png','cat_and_dog.png','screenshot.png','screenshot1.png','screenshot2.png','screenshot_2024.png',
// Тексты
'data.txt','data1.txt','data2.txt','data_old.txt','data_new.txt','notes.txt','notes1.txt',
'readme.txt','README.txt','readme.md','log.txt','file001.txt','file002.txt','file003.txt','file010.txt','file100.txt',
// Код
'script.py','script1.py','test.py','main.py','main_v2.py','app.py',
'index.html','style.css','script.js','app.js','main.js',
// Конфиги
'data.json','config.json','config.xml','settings.ini',
// Презентации
'presentation.pptx','слайд1.pptx','слайд2.pptx','слайд_итог.pptx',
// Медиа
'video.mp4','video1.mp4','clip.mp4','movie.mkv',
'song.mp3','song1.mp3','song2.mp3','podcast.mp3',
// Архивы
'archive.zip','archive.rar','archive1.zip','backup.zip','backup_2024.zip','backup_2025.zip','project.zip',
// Служебные
'temp.tmp','temp1.tmp','cache.tmp',
'system.log','error.log','access.log','debug.log',
// Тесты
'test1.docx','test2.docx','test3.docx','Тест_итог.docx',
// ── Файлы с двойными и более расширениями (для среднего уровня) ──
// Трапы .exe под видом документов
'отчёт.pdf.exe','приказ.docx.exe','счёт.pdf.exe',
// Резервные копии
'фото.jpg.bak','фото1.jpg.bak','фото_зима.jpg.bak','документ.docx.bak','таблица.xlsx.bak',
// Архивы
'archive.tar.gz','backup.tar.gz',
// Минифицированные файлы
'script.min.js','style.min.css',
// Версионированные документы
'report.v2.docx','report.v3.docx',
// Файлы с датой в расширении
'data.2024.csv','data.2025.csv','фото.2024.jpg','фото.2023.jpg',
// ── Дополнительно для ЕГЭ (реальные имена из заданий) ──
'aspera.doc','capers.dat','capers.doc','operas.doc','pergola.docx','prospero.docx',
'inmar.xls','komarik.xlsx','komarik.xxx','marka.xlsx','romario.xls','smart.xlsx',
'amir.dat','amir.data','mig.dat','comic.dat','demid.dat','femina.dat',
'chifera.dat','chifera.doc','ferrum.doc','deLafer.doc','oferta.doc','tokoferol.doc',
'primera.dat','primera.doc','merchant.doc','k-mer.doc','omerta.doc','Tamerlan.doc',
'bisquit.xlsx','cabinda.xls','cubic.xlsx','irbis.xls','tobias.xls','tobias.xml',
'slon.dat','slon.data','klon.dat','poklon.dat','lom.dat','lomka.dat',
'adob.dat','adob.data','dos.dat','pardon.dat','odor.dat','radost.dat',
'izoferen.doc','kifera.dat','kifera.doc','la-fer-atos.doc','merfer.docx','oferta.doc',
'comfort.c','cobalt.cpp','coat.c','cost.cpp',
'maveric.map','maveric.mp3','taverna.mp4','revolver.mp4','vera.mp3','zveri.mp3',
'syste.m','sys23.exe','system.dll','syszx.problem',
'1234.xls','23.xml','234.xls',
'gorod.ppt','horror.pptx','horror.prj','orcl.ppt','sordka.pptx','sort.ppt',
];
// ═══════════════════════════════════════════════════════════════
// УРОВЕНЬ 1: ПРОСТОЙ
// ═══════════════════════════════════════════════════════════════
const QUESTIONS_EASY = [
{ text: 'Все файлы с расширением .jpg', expected: '*.jpg' },
{ text: 'Все файлы с расширением .txt', expected: '*.txt' },
{ text: 'Все файлы с расширением .png', expected: '*.png' },
{ text: 'Все файлы с расширением .pdf', expected: '*.pdf' },
{ text: 'Все файлы с расширением .docx', expected: '*.docx' },
{ text: 'Все файлы с расширением .mp3', expected: '*.mp3' },
{ text: 'Все файлы, имя которых начинается с «отчёт»', expected: 'отчёт*' },
{ text: 'Все файлы, имя которых начинается с «фото»', expected: 'фото*' },
{ text: 'Все файлы, имя которых начинается с «data»', expected: 'data*' },
{ text: 'Все файлы, имя которых начинается с «screenshot»', expected: 'screenshot*' },
{ text: 'Все файлы, имя которых начинается с «backup»', expected: 'backup*' },
{ text: 'Все .jpg-файлы, имя которых начинается с «фото»', expected: 'фото*.jpg' },
{ text: 'Все .png-файлы, имя которых начинается с «screenshot»', expected: 'screenshot*.png' },
{ text: 'Все .txt-файлы, имя которых начинается с «file»', expected: 'file*.txt' },
{ text: 'Все .docx-файлы, имя которых начинается с «отчёт»', expected: 'отчёт*.docx' },
{ text: 'Все .zip-файлы, имя которых начинается с «backup»', expected: 'backup*.zip' },
{ text: 'Все файлы, в имени которых встречается «log»', expected: '*log*' },
{ text: 'Все файлы, в имени которых встречается «test»', expected: '*test*' },
{ text: 'Все .txt-файлы, имя которых — «data» и ровно одна цифра после', expected: 'data?.txt' },
{ text: 'Все .jpg-файлы, имя которых — «фото» и ровно одна цифра после', expected: 'фото?.jpg' },
];
// ═══════════════════════════════════════════════════════════════
// УРОВЕНЬ 2: СРЕДНИЙ
// Больше масок с ? + двойные расширения
// ═══════════════════════════════════════════════════════════════
const QUESTIONS_MIDDLE = [
// ── Маски с ? (7 заданий) ──
{ text: 'Все .txt-файлы, имя которых — «file» и ровно три символа после', expected: 'file???.txt' },
{ text: 'Все .txt-файлы, имя которых — «data» и ровно один символ после', expected: 'data?.txt' },
{ text: 'Все .jpg-файлы, имя которых — «фото» и ровно один символ после', expected: 'фото?.jpg' },
{ text: 'Все .png-файлы, имя которых — «screenshot» и ровно один символ после', expected: 'screenshot?.png' },
{ text: 'Все .docx-файлы, имя которых — «отчёт» и ровно один символ после', expected: 'отчёт?.docx' },
{ text: 'Все .py-файлы, имя которых — «script» и ровно один символ после', expected: 'script?.py' },
{ text: 'Все .tmp-файлы, имя которых — «temp» и ровно один символ после', expected: 'temp?.tmp' },
// ── Двойные и более расширения (9 заданий) ──
{ text: 'Все .exe-файлы, включая замаскированные под документы (например, «отчёт.pdf.exe»)', expected: '*.exe' },
{ text: 'Все файлы с расширением .bak (резервные копии)', expected: '*.bak' },
{ text: 'Все архивы .tar.gz', expected: '*.tar.gz' },
{ text: 'Все минифицированные JS-файлы (.min.js)', expected: '*.min.js' },
{ text: 'Все минифицированные CSS-файлы (.min.css)', expected: '*.min.css' },
{ text: 'Все файлы, в имени которых есть «.pdf.» (документы с двойным расширением)', expected: '*.pdf.*' },
{ text: 'Все .docx-файлы с версией вида .v2.docx или .v3.docx (ровно одна цифра после v)', expected: '*.v?.docx' },
{ text: 'Все файлы, в имени которых есть «.2024.» (2024 как отдельное расширение)', expected: '*.2024.*' },
{ text: 'Все фото с четырёхзначным годом в имени — например, «фото.2024.jpg»', expected: 'фото.????.jpg' },
// ── Комбинации (4 задания) ──
{ text: 'Все .exe-файлы, имя которых начинается с «отчёт»', expected: 'отчёт*.exe' },
{ text: 'Все .bak-файлы, имя которых начинается с «фото»', expected: 'фото*.bak' },
{ text: 'Все файлы, начинающиеся с «data» и содержащие минимум две точки', expected: 'data*.*.*' },
{ text: 'Все файлы, содержащие не менее двух точек в имени (то есть с двумя и более расширениями)', expected: '*.*.*' },
];
// ═══════════════════════════════════════════════════════════════
// УРОВЕНЬ 3: ЕГЭ (реальные маски из заданий типа Д7)
// ═══════════════════════════════════════════════════════════════
const QUESTIONS_EGE = [
{
text: 'В каталоге: aspera.doc, capers.dat, capers.doc, operas.doc, pergola.docx, prospero.docx. ' +
'Выберите файлы: aspera.doc, capers.doc, operas.doc, prospero.docx.',
expected: '*?per?*.do*'
},
{
text: 'В каталоге: inmar.xls, komarik.xlsx, komarik.xxx, marka.xlsx, romario.xls, smart.xlsx. ' +
'Выберите файлы: komarik.xlsx, marka.xlsx, romario.xls, smart.xlsx.',
expected: '*?mar*.xls*'
},
{
text: 'В каталоге: amir.dat, amir.data, mig.dat, comic.dat, demid.dat, femina.dat. ' +
'Выберите файлы: amir.dat, mig.dat, comic.dat, demid.dat.',
expected: '*mi*.dat'
},
{
text: 'В каталоге: chifera.dat, chifera.doc, ferrum.doc, deLafer.doc, oferta.doc, tokoferol.doc. ' +
'Выберите файлы, содержащие содержащих «fer» в имени и расширение .doc.',
expected: '*fer*.doc'
},
{
text: 'В каталоге: primera.dat, primera.doc, merchant.doc, k-mer.doc, omerta.doc, Tamerlan.doc. ' +
'Выберите файлы, содержащие содержащих «mer» в имени и расширение .doc.',
expected: '*mer*.doc'
},
{
text: 'В каталоге: bisquit.xlsx, cabinda.xls, cubic.xlsx, irbis.xls, tobias.xls, tobias.xml. ' +
'Выберите файлы: cabinda.xls, cubic.xlsx, irbis.xls, tobias.xls.',
expected: '??bi*.xls*'
},
{
text: 'В каталоге: slon.dat, slon.data, klon.dat, poklon.dat, lom.dat, lomka.dat. ' +
'Выберите файлы: slon.dat, klon.dat, poklon.dat, lom.dat.',
expected: '*lo?.dat'
},
{
text: 'В каталоге: adob.dat, adob.data, dos.dat, pardon.dat, odor.dat, radost.dat. ' +
'Выберите файлы: adob.dat, dos.dat, pardon.dat, odor.dat.',
expected: '*do?.dat'
},
{
text: 'В каталоге: izoferen.doc, kifera.dat, kifera.doc, la-fer-atos.doc, merfer.docx, oferta.doc. ' +
'Выберите файлы: izoferen.doc, kifera.doc, la-fer-atos.doc, oferta.doc.',
expected: '*?fer*?.do*'
},
{
text: 'В каталоге: comfort.c, cobalt.cpp, coat.c, cost.cpp. ' +
'Выберите все четыре файла: comfort.c, cobalt.cpp, coat.c, cost.cpp.',
expected: 'c*t.c??'
},
{
text: 'В каталоге: maveric.map, maveric.mp3, taverna.mp4, revolver.mp4, vera.mp3, zveri.mp3. ' +
'Выберите файлы: maveric.mp3, taverna.mp4, revolver.mp4, zveri.mp3.',
expected: '*?ver*.m*'
},
{
text: 'В каталоге: gorod.ppt, horror.pptx, horror.prj, orcl.ppt, sordka.pptx, sort.ppt. ' +
'Выберите файлы, имя которых содержит «or» и расширение начинается с .p.',
expected: '*or*.p*'
},
{
text: 'В каталоге: 1234.xls, 23.xml, 234.xls. ' +
'Выберите файлы: 1234.xls, 23.xml, 234.xls.',
expected: '*23*.?x*'
},
{
text: 'Какое из имён файлов НЕ удовлетворяет маске sys??.* ? ' +
'Варианты: syste.m, sys23.exe, system.dll, syszx.problem. ' +
'Введите маску, которая отберёт ровно те файлы, которые удовлетворяют исходной.',
expected: 'sys??.*'
},
{
text: 'Какое из имён файлов удовлетворяет маске ?mas*.p* ? ' +
'Варианты: massiv.pas, mmassiv.p, mmassiv.cpp, mas.p. ' +
'Введите маску, которая отберёт ровно правильные файлы.',
expected: '?mas*.p*'
},
];
// ── Состояние ────────────────────────────────────────────────
let currentLevel = 'easy';
let currentQuestions = [];
let currentIdx = 0;
let score = 0;
let answered = false;
// ── Утилиты ──────────────────────────────────────────────────
function maskToRegex(mask) {
let re = '';
for (const ch of mask) {
if (ch === '*') {
re += '.*';
} else if (ch === '?') {
re += '.';
} else if (/[.*+?^${}()|[\]\\]/.test(ch)) {
re += '\\' + ch;
} else {
re += ch;
}
}
return new RegExp('^' + re + '$', 'i');
}
function matchFiles(mask) {
const re = maskToRegex(mask);
return FILES.filter(f => re.test(f)).sort();
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({
'&':'&','<':'<','>':'>','"':'"',"'":'''
}[c]));
}
// ── Рендер вопроса ───────────────────────────────────────────
function renderQuestion() {
const q = currentQuestions[currentIdx];
document.getElementById('fmNum').textContent = currentIdx + 1;
document.getElementById('fmTotal').textContent = currentQuestions.length;
document.getElementById('fmScore').textContent = score;
document.getElementById('fmTask').textContent = q.text;
const input = document.getElementById('fmInput');
input.value = '';
input.disabled = false;
const submit = document.getElementById('fmSubmit');
submit.disabled = false;
submit.classList.remove('hidden');
document.getElementById('fmResult').classList.add('hidden');
document.getElementById('fmNext').classList.add('hidden');
answered = false;
setTimeout(() => input.focus(), 50);
}
// ── Рендер результата ────────────────────────────────────────
function renderResult(userMask, userFiles, targetFiles, isCorrect) {
document.getElementById('fmResult').classList.remove('hidden');
const verdictEl = document.getElementById('fmVerdict');
if (isCorrect) {
verdictEl.textContent = '✓ Верно!';
verdictEl.className = 'fm-verdict ok';
} else {
verdictEl.textContent = '✗ Неверно';
verdictEl.className = 'fm-verdict fail';
}
const infoEl = document.getElementById('fmMaskInfo');
let infoHTML = `Ваша маска: ${escapeHtml(userMask)}`;
if (!isCorrect) {
infoHTML += `
Правильная маска: ${escapeHtml(currentQuestions[currentIdx].expected)}`;
}
infoEl.innerHTML = infoHTML;
const targetSet = new Set(targetFiles);
const foundUl = document.getElementById('fmFound');
foundUl.innerHTML = '';
if (userFiles.length === 0) {
const li = document.createElement('li');
li.className = 'fm-empty';
li.textContent = 'Ни один файл не подошёл под маску';
foundUl.appendChild(li);
} else {
for (const f of userFiles) {
const li = document.createElement('li');
li.textContent = f;
if (!targetSet.has(f)) li.classList.add('fm-wrong');
foundUl.appendChild(li);
}
}
const userSet = new Set(userFiles);
const missed = targetFiles.filter(f => !userSet.has(f));
const missedWrap = document.getElementById('fmMissedWrap');
const missedUl = document.getElementById('fmMissed');
missedUl.innerHTML = '';
if (missed.length > 0) {
missedWrap.classList.remove('hidden');
for (const f of missed) {
const li = document.createElement('li');
li.textContent = f;
li.classList.add('fm-missed');
missedUl.appendChild(li);
}
} else {
missedWrap.classList.add('hidden');
}
const nextBtn = document.getElementById('fmNext');
nextBtn.classList.remove('hidden');
nextBtn.textContent = (currentIdx === currentQuestions.length - 1)
? 'Завершить'
: 'Следующий вопрос →';
}
// ── Итог ─────────────────────────────────────────────────────
function renderFinal() {
document.getElementById('fmMain').classList.add('hidden');
document.getElementById('fmFinal').classList.remove('hidden');
document.getElementById('fmFinalScore').textContent =
`${score} / ${currentQuestions.length}`;
const pct = score / currentQuestions.length;
let gradeText;
if (pct === 1) gradeText = 'Идеально! Все ответы верны.';
else if (pct >= 0.9) gradeText = 'Отлично!';
else if (pct >= 0.75) gradeText = 'Хорошо!';
else if (pct >= 0.5) gradeText = 'Неплохо, но есть что подтянуть.';
else gradeText = 'Стоит повторить тему «Маски файлов».';
document.getElementById('fmFinalGrade').textContent = gradeText;
}
// ── Логика ───────────────────────────────────────────────────
function submit() {
if (answered) return;
const input = document.getElementById('fmInput');
const mask = input.value.trim();
if (!mask) { input.focus(); return; }
answered = true;
input.disabled = true;
const submitBtn = document.getElementById('fmSubmit');
submitBtn.disabled = true;
submitBtn.classList.add('hidden');
const q = currentQuestions[currentIdx];
const targetFiles = matchFiles(q.expected);
const userFiles = matchFiles(mask);
const isCorrect = targetFiles.length === userFiles.length &&
targetFiles.every((f, i) => f === userFiles[i]);
if (isCorrect) {
score++;
document.getElementById('fmScore').textContent = score;
}
renderResult(mask, userFiles, targetFiles, isCorrect);
}
function next() {
if (currentIdx === currentQuestions.length - 1) {
renderFinal();
} else {
currentIdx++;
renderQuestion();
}
}
// ── Стартовый экран и уровни ────────────────────────────────
function showStart() {
document.getElementById('fmStart').classList.remove('hidden');
document.getElementById('fmMain').classList.add('hidden');
document.getElementById('fmFinal').classList.add('hidden');
}
function startLevel(level) {
currentLevel = level;
if (level === 'ege') currentQuestions = QUESTIONS_EGE;
else if (level === 'middle') currentQuestions = QUESTIONS_MIDDLE;
else currentQuestions = QUESTIONS_EASY;
currentIdx = 0;
score = 0;
answered = false;
document.getElementById('fmStart').classList.add('hidden');
document.getElementById('fmMain').classList.remove('hidden');
document.getElementById('fmFinal').classList.add('hidden');
renderQuestion();
}
// ── Инициализация ────────────────────────────────────────────
function init() {
if (!document.getElementById('fmInput')) return;
document.getElementById('fmLevelEasy').addEventListener('click', () => startLevel('easy'));
document.getElementById('fmLevelMiddle').addEventListener('click', () => startLevel('middle'));
document.getElementById('fmLevelEge').addEventListener('click', () => startLevel('ege'));
document.getElementById('fmSubmit').addEventListener('click', submit);
document.getElementById('fmNext').addEventListener('click', next);
document.getElementById('fmRestart').addEventListener('click', () => startLevel(currentLevel));
document.getElementById('fmChangeLevel').addEventListener('click', showStart);
document.getElementById('fmInput').addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); submit(); }
});
showStart();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}