// Состояние плеера let width = 15; let height = 15; let map = []; let solution = null; let trajectory = []; let currentStep = 0; let isPlaying = false; let playbackSpeed = 5; let playbackInterval = null; let startPosition = null; let scale = 1; let offsetX = 0; let offsetY = 0; let isPanning = false; let startPanX = 0; let startPanY = 0; // Функция обработки ошибок во время воспроизведения function handlePlaybackError(message) { alert(`❌ Ошибка воспроизведения: ${message}`); pauseVisualization(); // Сбрасываем счёт, но не очищаем визуализацию if (trajectory && trajectory.length > 0) { trajectory.forEach(step => { step.score = 0; step.passedCheckpoints = 0; }); updateStepInfo(); drawMap(); } console.error('Ошибка воспроизведения:', message); } // Функция проверки корректности ускорения function validateAcceleration(ax, ay, x, y, vx, vy) { // Проверка выезда за границы карты if (x < 0 || x >= width || y < 0 || y >= height) { return `Выезд за границы карты на позиции (${x.toFixed(1)}, ${y.toFixed(1)})`; } // Получаем тип текущей ячейки const cellX = Math.floor(x); const cellY = Math.floor(y); const cellType = map[cellY] ? map[cellY][cellX] : -1; // Проверка правил для разных типов ячеек switch (cellType) { case 0: // Обычная дорога if (ax < -2 || ax > 2 || ay < -2 || ay > 2) { return `На дороге ускорение должно быть от -2 до +2, получено (${ax}, ${ay})`; } break; case 1: // Камень // Можно проезжать через камни, но нельзя на них останавливаться if (vx === 0 && vy === 0 && ax === 0 && ay === 0) { return `Нельзя останавливаться на камне на позиции (${cellX}, ${cellY})`; } if (ax < -2 || ax > 2 || ay < -2 || ay > 2) { return `На камне ускорение должно быть от -2 до +2, получено (${ax}, ${ay})`; } break; case 2: // Снег if (ax < -1 || ax > 1 || ay < -1 || ay > 1) { return `На снегу ускорение должно быть от -1 до +1, получено (${ax}, ${ay})`; } break; case 3: // Лёд if (ax !== 0 || ay !== 0) { return `На льду нельзя менять ускорение, получено (${ax}, ${ay})`; } break; case 4: // Чекпоинт if (ax < -2 || ax > 2 || ay < -2 || ay > 2) { return `На чекпоинте ускорение должно быть от -2 до +2, получено (${ax}, ${ay})`; } break; case 5: // Старт if (ax < -2 || ax > 2 || ay < -2 || ay > 2) { return `На старте ускорение должно быть от -2 до +2, получено (${ax}, ${ay})`; } break; default: return `Неизвестный тип ячейки ${cellType} на позиции (${cellX}, ${cellY})`; } return null; // Нет ошибок } // Canvas элементы const canvas = document.getElementById('mapCanvas'); const ctx = canvas.getContext('2d'); // Инициализация function init() { initMap(); resizeCanvas(); drawMap(); updateControlsState(); updateStepInfo(); } // Инициализация пустой карты function initMap() { map = Array(height).fill(null).map(() => Array(width).fill(0)); } function resizeCanvas() { const wrapper = canvas.parentElement; canvas.width = Math.max(1000, wrapper.clientWidth || 1000); canvas.height = Math.max(1000, wrapper.clientHeight || 1000); } // Отрисовка карты function drawMap() { ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.translate(offsetX, offsetY); ctx.scale(scale, scale); drawCells(ctx, map, width, height); drawGrid(ctx, width, height); drawMarkers(ctx, map, width, height); if (trajectory.length > 0) { drawTrajectory(); } } // Рисование траектории решения function drawTrajectory() { if (!trajectory || trajectory.length === 0) return; // Рисуем все предыдущие позиции как след ctx.strokeStyle = '#667eea'; ctx.lineWidth = 3; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); for (let i = 0; i <= currentStep && i < trajectory.length; i++) { const pos = trajectory[i]; const screenX = pos.x * CELL_SIZE + CELL_SIZE / 2; const screenY = pos.y * CELL_SIZE + CELL_SIZE / 2; if (i === 0) { ctx.moveTo(screenX, screenY); } else { ctx.lineTo(screenX, screenY); } } ctx.stroke(); // Рисуем точки на каждом шаге for (let i = 0; i <= currentStep && i < trajectory.length; i++) { const pos = trajectory[i]; const screenX = pos.x * CELL_SIZE + CELL_SIZE / 2; const screenY = pos.y * CELL_SIZE + CELL_SIZE / 2; ctx.beginPath(); ctx.arc(screenX, screenY, 4, 0, Math.PI * 2); ctx.fillStyle = '#667eea'; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); } // Рисуем текущую позицию большим кругом if (currentStep < trajectory.length) { const current = trajectory[currentStep]; const screenX = current.x * CELL_SIZE + CELL_SIZE / 2; const screenY = current.y * CELL_SIZE + CELL_SIZE / 2; // Пульсирующий эффект ctx.beginPath(); ctx.arc(screenX, screenY, 10, 0, Math.PI * 2); ctx.fillStyle = '#f5576c'; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 3; ctx.stroke(); // Стрелка направления скорости if (current.vx !== 0 || current.vy !== 0) { const arrowLen = 20; const angle = Math.atan2(current.vy, current.vx); const endX = screenX + Math.cos(angle) * arrowLen; const endY = screenY + Math.sin(angle) * arrowLen; ctx.beginPath(); ctx.moveTo(screenX, screenY); ctx.lineTo(endX, endY); ctx.strokeStyle = '#f5576c'; ctx.lineWidth = 3; ctx.stroke(); // Наконечник стрелки const headLen = 8; const headAngle = Math.PI / 6; ctx.beginPath(); ctx.moveTo(endX, endY); ctx.lineTo( endX - headLen * Math.cos(angle - headAngle), endY - headLen * Math.sin(angle - headAngle) ); ctx.moveTo(endX, endY); ctx.lineTo( endX - headLen * Math.cos(angle + headAngle), endY - headLen * Math.sin(angle + headAngle) ); ctx.stroke(); } } } // Симуляция траектории на основе векторов ускорений function simulateTrajectory(accelerations, start) { const traj = []; let x = start.x; let y = start.y; let vx = 0; let vy = 0; let passedCheckpoints = new Set(); let totalSteps = accelerations.length; // Начальная позиция traj.push({ x, y, vx, vy, ax: 0, ay: 0, passedCheckpoints: 0, score: 0 }); // Применяем каждое ускорение for (let i = 0; i < accelerations.length; i++) { const [ax, ay] = accelerations[i]; // Обновляем скорость vx += ax; vy += ay; // Обновляем позицию x += vx; y += vy; // Проверяем, попали ли на чекпоинт const checkpointKey = `${Math.floor(x)},${Math.floor(y)}`; if (map[Math.floor(y)] && map[Math.floor(y)][Math.floor(x)] === 4) { passedCheckpoints.add(checkpointKey); } // Вычисляем очки по формуле: 100 * количество пройденных чекпоинтов ^ 1.3 / количество шагов const checkpointCount = passedCheckpoints.size; const score = totalSteps > 0 ? Math.round(100 * Math.pow(checkpointCount, 1.3) / totalSteps) : 0; traj.push({ x, y, vx, vy, ax, ay, passedCheckpoints: checkpointCount, score: score }); } return traj; } // Загрузка карты из JSON function loadMap(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); const validated = validateMap(data); // Импортируем карту height = validated.height; width = validated.width; map = validated.map; // Очищаем траекторию при загрузке новой карты clearVisualization(); resizeCanvas(); drawMap(); // Включаем кнопку "Найти решение" после загрузки карты document.getElementById('solveBtn').disabled = false; document.getElementById('loadMapBtn').textContent = '✓ Карта загружена'; setTimeout(() => { document.getElementById('loadMapBtn').textContent = '📂 Загрузить карту'; }, 2000); alert('Карта успешно загружена!'); } catch (error) { alert('Ошибка при загрузке карты: ' + error.message); console.error('Ошибка загрузки:', error); } }; reader.readAsText(file); event.target.value = ''; } // Загрузка решения из JSON function loadSolution(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); if (!data.solution || !Array.isArray(data.solution)) { throw new Error('Неверный формат: отсутствует массив solution'); } // Проверяем, что это массив массивов из двух чисел if (!data.solution.every(acc => Array.isArray(acc) && acc.length === 2)) { throw new Error('Неверный формат: solution должен быть массивом [[ax, ay], ...]'); } // Находим стартовую позицию startPosition = findStartPosition(map, width, height); if (!startPosition) { throw new Error('На карте не найдена точка старта (тип 5). Сначала загрузите карту с точкой старта.'); } solution = data.solution; trajectory = simulateTrajectory(solution, startPosition); currentStep = 0; // Обновляем состояние кнопок и информацию updateControlsState(); updateStepInfo(); drawMap(); document.getElementById('loadSolutionBtn').textContent = '✓ Решение загружено'; setTimeout(() => { document.getElementById('loadSolutionBtn').textContent = '🎬 Загрузить решение'; }, 2000); alert(`Решение загружено! ${solution.length} шагов.`); } catch (error) { alert('Ошибка при загрузке решения: ' + error.message); console.error('Ошибка загрузки:', error); } }; reader.readAsText(file); event.target.value = ''; } // Обновление состояния элементов управления function updateControlsState() { const hasTrajectory = trajectory && trajectory.length > 0; document.getElementById('playBtn').disabled = !hasTrajectory; document.getElementById('pauseBtn').disabled = !hasTrajectory || !isPlaying; document.getElementById('resetBtn').disabled = !hasTrajectory; document.getElementById('backBtn').disabled = !hasTrajectory; document.getElementById('forwardBtn').disabled = !hasTrajectory; document.getElementById('speedSlider').disabled = !hasTrajectory; } // Обновление информации о текущем шаге function updateStepInfo() { if (!trajectory || trajectory.length === 0) { document.getElementById('stepNumber').textContent = '0 / 0'; document.getElementById('positionValue').textContent = '(0, 0)'; document.getElementById('velocityValue').textContent = '(0, 0)'; document.getElementById('accelerationValue').textContent = '(0, 0)'; document.getElementById('scoreValue').textContent = '0'; return; } const current = trajectory[currentStep]; document.getElementById('stepNumber').textContent = `${currentStep} / ${trajectory.length - 1}`; document.getElementById('positionValue').textContent = `(${current.x}, ${current.y})`; document.getElementById('velocityValue').textContent = `(${current.vx}, ${current.vy})`; document.getElementById('accelerationValue').textContent = `(${current.ax}, ${current.ay})`; document.getElementById('scoreValue').textContent = current.score || 0; } // Воспроизведение визуализации function playVisualization() { if (!trajectory || trajectory.length === 0) return; isPlaying = true; updateControlsState(); playbackInterval = setInterval(() => { if (currentStep < trajectory.length - 1) { const nextStep = currentStep + 1; const current = trajectory[currentStep]; const next = trajectory[nextStep]; // Проверяем корректность ускорения для следующего шага const error = validateAcceleration(next.ax, next.ay, current.x, current.y, current.vx, current.vy); if (error) { handlePlaybackError(`Шаг ${nextStep + 1}: ${error}`); return; } // Проверяем корректность новой позиции const positionError = validateAcceleration(0, 0, next.x, next.y, next.vx, next.vy); if (positionError && positionError.includes('Выезд за границы')) { handlePlaybackError(`Шаг ${nextStep + 1}: ${positionError}`); return; } currentStep = nextStep; updateStepInfo(); drawMap(); } else { pauseVisualization(); } }, 1000 / playbackSpeed); } // Пауза воспроизведения function pauseVisualization() { isPlaying = false; updateControlsState(); if (playbackInterval) { clearInterval(playbackInterval); playbackInterval = null; } } // Сброс визуализации function resetVisualization() { pauseVisualization(); currentStep = 0; updateStepInfo(); drawMap(); } // Шаг вперед function stepForward() { if (!trajectory || trajectory.length === 0) return; if (currentStep < trajectory.length - 1) { const nextStep = currentStep + 1; const current = trajectory[currentStep]; const next = trajectory[nextStep]; // Проверяем корректность ускорения для следующего шага const error = validateAcceleration(next.ax, next.ay, current.x, current.y, current.vx, current.vy); if (error) { handlePlaybackError(`Шаг ${nextStep + 1}: ${error}`); return; } // Проверяем корректность новой позиции const positionError = validateAcceleration(0, 0, next.x, next.y, next.vx, next.vy); if (positionError && positionError.includes('Выезд за границы')) { handlePlaybackError(`Шаг ${nextStep + 1}: ${positionError}`); return; } currentStep = nextStep; updateStepInfo(); drawMap(); } } // Шаг назад function stepBackward() { if (!trajectory || trajectory.length === 0) return; if (currentStep > 0) { currentStep--; updateStepInfo(); drawMap(); } } // Обновление скорости воспроизведения function updateSpeed() { playbackSpeed = parseInt(document.getElementById('speedSlider').value); document.getElementById('speedValue').textContent = `${playbackSpeed}x`; // Если воспроизведение идет, перезапускаем с новой скоростью if (isPlaying) { pauseVisualization(); playVisualization(); } } // Очистка визуализации function clearVisualization() { pauseVisualization(); solution = null; trajectory = []; currentStep = 0; startPosition = null; updateControlsState(); updateStepInfo(); drawMap(); } // Решение карты через API async function solveMap() { const solveBtn = document.getElementById('solveBtn'); const statusDiv = document.getElementById('solverStatus'); // Проверяем, что карта загружена if (!map || map.length === 0) { alert('⚠️ Сначала загрузите карту!'); return; } // Находим стартовую позицию startPosition = findStartPosition(map, width, height); if (!startPosition) { alert('⚠️ На карте не найдена точка старта (тип 5)!'); return; } // Проверяем наличие чекпоинтов let hasCheckpoint = false; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (map[y][x] === 4) { hasCheckpoint = true; break; } } if (hasCheckpoint) break; } if (!hasCheckpoint) { alert('⚠️ На карте нет чекпоинтов (тип 4)!'); return; } // Отключаем кнопку и показываем статус solveBtn.disabled = true; solveBtn.textContent = '⏳ Поиск решения...'; statusDiv.innerHTML = '
./run-webservice.sh