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();
|