409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
/* ═══════════════════════════════════════════════
|
||
Pathfinder AI — A* Görsel Labirent Çözücü
|
||
Grid: 25x25 | Duvar çizme, start/goal sürükleme,
|
||
açı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();
|