pathfinder-ai/public/app.js

409 lines
12 KiB
JavaScript
Raw 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.

/* ═══════════════════════════════════════════════
Pathfinder AI — A* Görsel Labirent Çözücü
Grid: 25x25 | Duvar çizme, start/goal sürükleme,
ık mavi ziyaret animasyonu, sarı en-kısa-yol
ve "Yol Bulunamadı" kırmızı neon tostu.
═══════════════════════════════════════════════ */
const SIZE = 25;
// ─── Grid State ───
let grid = []; // 2D: 0 = boş, 1 = duvar
let startPos = { r: 0, c: 2 };
let goalPos = { r: SIZE - 1, c: SIZE - 2 };
let cells = []; // DOM referansları (2D)
// ─── Animasyon State ───
let animRunning = false;
let animTimer = null;
let animSpeed = 6; // hücre / kare
let visitedQueue = []; // ziyaret edilecek hücreler sırası
let currentQueue = [];
let pathCells = [];
let stateLocked = false; // animasyon sırasında grid kilitli
// ─── DOM ───
const gridEl = document.getElementById('grid');
const statsVisited = document.getElementById('visited-count');
const statsPath = document.getElementById('path-length');
const toast = document.getElementById('toast');
const btnBul = document.getElementById('btn-bul');
const btnSifirla = document.getElementById('btn-sifirla');
const btnTemizle = document.getElementById('btn-temizle');
const speedSlider = document.getElementById('speed');
const speedValue = document.getElementById('speed-value');
// ─── Grid Oluştur ───
function initGrid() {
gridEl.innerHTML = '';
grid = [];
cells = [];
for (let r = 0; r < SIZE; r++) {
grid[r] = [];
cells[r] = [];
for (let c = 0; c < SIZE; c++) {
grid[r][c] = 0;
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.r = r;
cell.dataset.c = c;
gridEl.appendChild(cell);
cells[r][c] = cell;
}
}
placeAgent(startPos, 'start');
placeAgent(goalPos, 'goal');
bindGridEvents();
}
function placeAgent(pos, type) {
// Diğer agenti temizle
document.querySelectorAll('.cell.start').forEach(x => x.classList.remove('start', 'dragging'));
document.querySelectorAll('.cell.goal').forEach(x => x.classList.remove('goal', 'dragging'));
const cell = cells[pos.r] ? cells[pos.r][pos.c] : undefined;
if (!cell) return;
cell.classList.add(type);
if (type === 'start') startPos = { ...pos };
else goalPos = { ...pos };
}
// ─── Hücre sınıfını temizle (renkleri) ───
function clearVisuals() {
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
cells[r][c].classList.remove('visited', 'current', 'path', 'wall', 'start', 'goal');
}
}
placeAgent(startPos, 'start');
placeAgent(goalPos, 'goal');
statsPath.textContent = 'Yol: —';
}
function removeVisualsOnly() {
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
cells[r][c].classList.remove('visited', 'current', 'path');
}
}
}
// ─── Surükleyerek Duvar Çizme + Start/Goal Taşıma ───
function bindGridEvents() {
let isMouseDown = false;
let paintMode = 'wall'; // wall paint
let draggingAgent = null; // 'start' | 'goal'
let lastPainted = null;
gridEl.addEventListener('pointerdown', (e) => {
if (stateLocked) return;
const cell = e.target.closest('.cell');
if (!cell) return;
isMouseDown = true;
if (cell.classList.contains('start')) {
draggingAgent = 'start';
cell.classList.add('dragging');
return;
}
if (cell.classList.contains('goal')) {
draggingAgent = 'goal';
cell.classList.add('dragging');
return;
}
e.preventDefault();
const r = +cell.dataset.r, c = +cell.dataset.c;
// duvarı çiz ya da sil
const isWall = cell.classList.contains('wall');
paintSet(cell, r, c, isWall);
lastPainted = cell.dataset.r + ',' + cell.dataset.c;
});
gridEl.addEventListener('pointermove', (e) => {
if (!isMouseDown) return;
if (stateLocked) return;
const cell = e.target.closest('.cell');
if (!cell) return;
// Start/Goal sürükleniyor → konumunu ara
if (draggingAgent) {
const r = +cell.dataset.r, c = +cell.dataset.c;
// hedef hücre duvar SAKIZMAZ; start/goal kendi yeriyse dokunma
if (cell.classList.contains('wall')) {
// duvar üzerine bırakmaya çalışma: duvarı çıkarıp agenti koy
cell.classList.remove('wall');
grid[r][c] = 0;
}
// kendi agent'inin yeri değişir
if (draggingAgent === 'start' && (r !== startPos.r || c !== startPos.c)) {
removeClassAt(startPos, 'start');
startPos = { r, c };
cell.classList.add('start');
} else if (draggingAgent === 'goal' && (r !== goalPos.r || c !== goalPos.c)) {
removeClassAt(goalPos, 'goal');
goalPos = { r, c };
cell.classList.add('goal');
}
return;
}
// Duvar boyama / silme (dedup: aynı hücreye sürekli tetiklenmesin)
const key = cell.dataset.r + ',' + cell.dataset.c;
if (key === lastPainted) return;
lastPainted = key;
// Agent hücresinin üzerinden boyamayı engelle
if (cell.classList.contains('start') || cell.classList.contains('goal')) return;
const r = +cell.dataset.r, c = +cell.dataset.c;
const isWallNow = cell.classList.contains('wall');
paintSet(cell, r, c, isWallNow);
});
// Mouse kaldırma (dokunma dahil)
window.addEventListener('pointerup', () => {
isMouseDown = false;
draggingAgent = null;
document.querySelectorAll('.cell.dragging').forEach(x => x.classList.remove('dragging'));
});
// Sürükleme başlarken global varsayılanı engelle
gridEl.addEventListener('dragstart', (e) => e.preventDefault());
}
function removeClassAt(pos, cls) {
const c = cells[pos.r] ? cells[pos.r][pos.c] : undefined;
if (c) c.classList.remove(cls);
}
function paintSet(cell, r, c, erase) {
const isAgentCell = cell.classList.contains('start') || cell.classList.contains('goal');
if (isAgentCell) return;
if (erase) {
cell.classList.remove('wall');
grid[r][c] = 0;
} else {
cell.classList.add('wall');
grid[r][c] = 1;
}
}
// ─── A* Algoritması (iteratif, yolu + ziyaret sırasını döndürür) ───
function astar() {
// Ön koşul: start/goal koordinatları
const sr = startPos.r, sc = startPos.c;
const gr = goalPos.r, gc = goalPos.c;
// Start veya goal duvar üzerindeyse → anında "yol yok"
if (grid[sr][sc] === 1 || grid[gr][gc] === 1) return { found: false, visitedSeq: [] };
// open, closed, g, f, came-from
const open = new Map(); // "r,c" → f
const closed = new Set();
const g = new Map();
const f = new Map();
const came = new Map();
const key = (r, c) => r + ',' + c;
const h = (r, c) => Math.abs(r - gr) + Math.abs(c - gc); // Manhattan
const sk = key(sr, sc);
g.set(sk, 0);
f.set(sk, h(sr, sc));
open.set(sk, f.get(sk));
const visitedSeq = [];
const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
while (open.size > 0) {
// En küçük f'li düğümü seç
let bestK = null, bestF = Infinity;
for (const [k, val] of open) {
if (val < bestF) { bestF = val; bestK = k; }
}
const [cr, cc] = bestK.split(',').map(Number);
open.delete(bestK);
closed.add(bestK);
visitedSeq.push({ r: cr, c: cc });
if (cr === gr && cc === gc) {
// Yolu geri izle
const path = [];
let cur = bestK;
while (cur !== undefined) {
const [pr, pc] = cur.split(',').map(Number);
path.push({ r: pr, c: pc });
cur = came.get(cur);
}
path.reverse();
return { found: true, visitedSeq, path };
}
for (const [dr, dc] of dirs) {
const nr = cr + dr, nc = cc + dc;
if (nr < 0 || nc < 0 || nr >= SIZE || nc >= SIZE) continue;
if (grid[nr][nc] === 1) continue; // duvar
const nk = key(nr, nc);
if (closed.has(nk)) continue;
const tentativeG = g.get(bestK) + 1;
if (!open.has(nk) || tentativeG < (g.get(nk) ?? Infinity)) {
came.set(nk, bestK);
g.set(nk, tentativeG);
const nf = tentativeG + h(nr, nc);
f.set(nk, nf);
open.set(nk, nf);
}
}
}
return { found: false, visitedSeq };
}
// ─── Animasyon Motoru ───
function startFind() {
if (stateLocked) return;
// Önceki animasyonu temizle
stopAnim();
removeVisualsOnly();
hideToast();
const result = astar();
if (!result.found) {
// Yol yok → kırmızı neon tost; grid kilidi açık kalsın
statsVisited.textContent = 'Ziyaret: 0';
statsPath.textContent = 'Yol: —';
showToast();
return;
}
// Bulundu — animasyon kuyruklarını kur
visitedQueue = result.visitedSeq;
pathCells = result.path || [];
currentQueue = [];
animRunning = true;
const vCountEl = document.getElementById('visited-count');
let idx = 0;
const totalVisited = visitedQueue.length;
const finalPath = result.path;
animTimer = setInterval(() => {
if (!animRunning) { clearInterval(animTimer); return; }
// Ziyaret edilenleri kademeli mavi yap
for (let step = 0; step < animSpeed && idx < totalVisited; step++, idx++) {
const { r, c } = visitedQueue[idx];
// start/goal/path hücrelerini ezme
if (finalPath && finalPath.some(p => p.r === r && p.c === c)) continue;
if (cells[r][c].classList.contains('start') || cells[r][c].classList.contains('goal')) continue;
if (!cells[r][c].classList.contains('visited')) {
// "current" flash: önce eski current'ı temizle
document.querySelectorAll('.cell.current').forEach(x => x.classList.remove('current'));
cells[r][c].classList.add('current');
// kısa sonra visited olsun
setTimeout(() => {
cells[r][c].classList.remove('current');
if (!cells[r][c].classList.contains('path') &&
!cells[r][c].classList.contains('start') &&
!cells[r][c].classList.contains('goal')) {
cells[r][c].classList.add('visited');
}
}, 50);
}
}
statsVisited.textContent = `Ziyaret: ${Math.min(idx, totalVisited)}`;
// Ziyaret bitti mi?
if (idx >= totalVisited) {
clearInterval(animTimer);
animTimer = null;
// Yolu çiz (sarı parlak)
setTimeout(() => {
statsPath.textContent = `Yol uzunluğu: ${finalPath.length - 1} adım`;
for (const { r, c } of finalPath) {
if (cells[r][c].classList.contains('start') || cells[r][c].classList.contains('goal')) continue;
cells[r][c].classList.add('path');
}
animRunning = false;
stateLocked = false;
btnBul.disabled = false;
btnBul.textContent = '🚀 Yolu Bul';
}, 120);
}
}, 30);
}
function stopAnim() {
if (animTimer) { clearInterval(animTimer); animTimer = null; }
animRunning = false;
currentQueue = [];
visitedQueue = [];
document.querySelectorAll('.cell.current').forEach(x => x.classList.remove('current'));
stateLocked = false;
}
// ─── Toast ───
function showToast() {
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 4200);
}
function hideToast() { toast.classList.remove('show'); }
// ─── Reset / Temizle ───
function resetAll() {
stopAnim();
hideToast();
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
cells[r][c].classList.remove('wall', 'visited', 'current', 'path', 'start', 'goal');
grid[r][c] = 0;
}
}
startPos = { r: 0, c: 2 };
goalPos = { r: SIZE - 1, c: SIZE - 2 };
placeAgent(startPos, 'start');
placeAgent(goalPos, 'goal');
statsVisited.textContent = 'Ziyaret: 0';
statsPath.textContent = 'Yol: —';
}
function clearWalls() {
stopAnim();
hideToast();
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
cells[r][c].classList.remove('wall');
grid[r][c] = 0;
}
}
removeVisualsOnly();
statsPath.textContent = 'Yol: —';
}
// ─── Kontrol Butonları ───
btnBul.addEventListener('click', () => {
if (btnBul.disabled) return;
startFind();
});
btnSifirla.addEventListener('click', () => {
resetAll();
hideToast();
});
btnTemizle.addEventListener('click', clearWalls);
speedSlider.addEventListener('input', (e) => {
animSpeed = +e.target.value;
speedValue.textContent = e.target.value;
});
// ─── Başlat ───
initGrid();