559 lines
19 KiB
JavaScript
559 lines
19 KiB
JavaScript
// Состояние плеера
|
||
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('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();
|
||
}
|
||
|
||
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();
|
||
|