Files

681 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Состояние плеера
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 = '<div style="color: #ff9800;">⏳ Отправка запроса на сервер...</div>';
try {
const requestData = {
map: map,
maxIterations: 5000000
};
const response = await fetch('http://localhost:5000/solve', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
// Успешно найдено решение
const stats = result.statistics;
statusDiv.innerHTML = `
<div style="color: #4caf50; font-weight: bold;">✅ Решение найдено!</div>
<div style="font-size: 12px; margin-top: 5px;">
📊 Шагов: ${stats.steps}<br>
🎯 Чекпоинтов: ${stats.checkpoints}<br>
⚡ Макс. скорость: ${stats.maxSpeed}<br>
⏱️ Время: ${stats.computeTimeSeconds.toFixed(2)}s
</div>
`;
// Загружаем решение напрямую
solution = result.solution;
trajectory = simulateTrajectory(solution, startPosition);
currentStep = 0;
// Обновляем состояние кнопок и информацию
updateControlsState();
updateStepInfo();
drawMap();
// Автоматически начинаем воспроизведение
setTimeout(() => {
playVisualization();
}, 500);
} else {
// Решение не найдено
statusDiv.innerHTML = `
<div style="color: #f44336; font-weight: bold;">❌ Решение не найдено</div>
<div style="font-size: 12px; margin-top: 5px;">${result.error || 'Неизвестная ошибка'}</div>
`;
}
} catch (error) {
console.error('Ошибка при решении карты:', error);
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
statusDiv.innerHTML = `
<div style="color: #f44336; font-weight: bold;">❌ Сервер недоступен</div>
<div style="font-size: 12px; margin-top: 5px;">
Убедитесь, что сервер запущен:<br>
<code style="background: #333; padding: 2px 5px; border-radius: 3px;">./run-webservice.sh</code>
</div>
`;
} else {
statusDiv.innerHTML = `
<div style="color: #f44336; font-weight: bold;">❌ Ошибка</div>
<div style="font-size: 12px; margin-top: 5px;">${error.message}</div>
`;
}
} finally {
// Включаем кнопку обратно
solveBtn.disabled = false;
solveBtn.textContent = '🧠 Найти решение';
}
}
canvas.addEventListener('mousedown', (e) => {
if (e.button === 2 || e.shiftKey) {
isPanning = true;
startPanX = e.clientX - offsetX;
startPanY = e.clientY - offsetY;
canvas.style.cursor = 'grabbing';
}
});
canvas.addEventListener('mousemove', (e) => {
if (isPanning) {
offsetX = e.clientX - startPanX;
offsetY = e.clientY - startPanY;
drawMap();
}
});
canvas.addEventListener('mouseup', () => {
isPanning = false;
canvas.style.cursor = 'default';
});
canvas.addEventListener('mouseleave', () => {
isPanning = false;
canvas.style.cursor = 'default';
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const zoom = e.deltaY < 0 ? 1.1 : 0.9;
const newScale = Math.min(Math.max(0.1, scale * zoom), 5);
offsetX = mouseX - (mouseX - offsetX) * (newScale / scale);
offsetY = mouseY - (mouseY - offsetY) * (newScale / scale);
scale = newScale;
drawMap();
});
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
window.addEventListener('resize', () => {
resizeCanvas();
drawMap();
});
init();