Spaces:
Running
Running
Download sim.js from atasoglu/degnekci-simulation: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/spaces/atasoglu/degnekci-simulation/resolve/main/sim.js
- Command line
-
hf download hf://spaces/atasoglu/degnekci-simulation/sim.js
-
curl -L -o sim.js https://huggingface.co/spaces/atasoglu/degnekci-simulation/resolve/main/sim.js
15.8 kB
| // Oyun çekirdeği: fizik, seviye kurulumu, çarpışma, park kontrolü. DOM'a dokunmaz; | |
| // tarayıcıda <script> ile, Node'da (test/bot.js) require ile yüklenir. Sürüş sabitleri game.py'den | |
| // alındı; araç tipleri ve seviyeler yalnız web sürümünde var. | |
| // Araç tipleri: L boy, W en, wb dingil mesafesi (dönüş yarıçapını belirler) | |
| const VEHICLES = { | |
| hatchback: { L: 70, W: 38, wb: 46 }, | |
| sedan: { L: 82, W: 40, wb: 52 }, | |
| kamyonet: { L: 88, W: 42, wb: 56 }, | |
| van: { L: 86, W: 44, wb: 56 }, | |
| tir: { L: 200, W: 50, wb: 120 }, | |
| }; | |
| const CEP_TIPLERI = ["hatchback", "sedan", "kamyonet", "van"]; // cebe sığanlar | |
| const COLORS = ["#e2483d", "#2f6fb2", "#e9e2d0", "#8fc7a3", "#b86b2c", "#9b7fd1", "#f28fb0", "#4fb3bf", "#6f7686", "#ffc93c"]; | |
| const CREEP = 18; // px/sn, ileri/geri sonrası araç bu hızla süzülür | |
| const SPEED_STEP = 8, MIN_SPEED = 6, MAX_SPEED = 45; // "hızlı"/"yavaş" adımı ve sınırları | |
| const BRAKE = 160; // px/sn² | |
| const MAX_STEER = (35 * Math.PI) / 180, STEER_RATE = (110 * Math.PI) / 180; | |
| const PARK_POINTS = 500, TIME_BONUS = 5, CRASH_PENALTY = 100; | |
| // yoldan geçen araçlar: önlerinde oyuncu varsa yavaşlayıp dururlar, yol açılınca düz devam ederler | |
| const TRAFFIC = { accel: 60, decel: 200, minGap: 4, spawnMin: 5, spawnMax: 11, perLane: 2, cruise: [55, 80], honkAfter: 1.5 }; | |
| // Oyun alanı: masaüstünde yatay, telefonda dikey. rows: 2 olan dünyada yolun altında karşı kaldırım ve | |
| // ikinci bir park sırası olur (dikey ekranın alt yarısı boş kalmasın) | |
| const WORLDS = { | |
| wide: { W: 900, H: 600, CURB: 70, nSpots: 10, rows: 1 }, | |
| tall: { W: 560, H: 720, CURB: 70, nSpots: 6, rows: 2 }, | |
| }; | |
| // kind: "dik" cebe burnu ya da arkası önde | "geri" cebe geri geri, burun yola bakar | "paralel" kaldırım boyu | |
| // Her seviyenin komutlarla çözülebildiği test/bot.js ile sınanır. | |
| const LEVELS = [ | |
| { name: "Çırak", kind: "dik", spotW: 92, spotD: 115, time: 90, points: 500, hint: "Yeşil park yerine burnunu sok" }, | |
| { name: "Kalfa", kind: "dik", spotW: 80, spotD: 115, time: 85, points: 600, hint: "Park yerleri daraldı" }, | |
| { name: "Geri Geri", kind: "geri", spotW: 88, spotD: 115, time: 100, points: 700, hint: "Park yerine geri geri gir, burun yola baksın" }, | |
| { name: "Usta", kind: "dik", spotW: 72, spotD: 115, time: 80, points: 800, hint: "Milim milim" }, | |
| { name: "Paralel", kind: "paralel", slotL: 110, gapL: 150, spotD: 62, time: 110, points: 900, hint: "Kaldırıma paralel park et" }, | |
| { name: "Değnekçi", kind: "geri", spotW: 76, spotD: 115, time: 90, points: 1000, hint: "Dar park yerine geri geri" }, | |
| ]; | |
| function corners(x, y, ang, length, width) { | |
| const c = Math.cos(ang), s = Math.sin(ang), hl = length / 2, hw = width / 2; | |
| return [[hl, hw], [hl, -hw], [-hl, -hw], [-hl, hw]].map(([dx, dy]) => [x + c * dx - s * dy, y + s * dx + c * dy]); | |
| } | |
| // İki dışbükey çokgen kesişiyor mu (ayırıcı eksen teoremi) | |
| function overlap(p, q) { | |
| for (const poly of [p, q]) { | |
| for (let i = 0; i < poly.length; i++) { | |
| const [x1, y1] = poly[i], [x2, y2] = poly[(i + 1) % poly.length]; | |
| const nx = y1 - y2, ny = x2 - x1; | |
| const a = p.map(([x, y]) => nx * x + ny * y), b = q.map(([x, y]) => nx * x + ny * y); | |
| if (Math.max(...a) < Math.min(...b) || Math.max(...b) < Math.min(...a)) return false; | |
| } | |
| } | |
| return true; | |
| } | |
| class Car { | |
| constructor(x, y, ang, type = "sedan", color = COLORS[0]) { | |
| Object.assign(this, { x, y, ang, type, color }, VEHICLES[type]); | |
| this.speed = 0; // hız büyüklüğü | |
| this.gear = 1; // 1 ileri, -1 geri | |
| this.steer = 0; | |
| this.steerTarget = 0; | |
| this.lastDir = 1; // "kır" için son yön (1 sağ, -1 sol) | |
| this.stopped = true; // ileri/geri denene kadar bekler | |
| } | |
| poly() { | |
| return corners(this.x, this.y, this.ang, this.L, this.W); | |
| } | |
| command(cmd) { | |
| if (cmd === "hızlı" || cmd === "yavaş") { | |
| if (this.stopped) return; // duran aracı yalnız ileri/geri kaldırır | |
| const step = cmd === "hızlı" ? SPEED_STEP : -SPEED_STEP; | |
| this.speed = Math.min(MAX_SPEED, Math.max(MIN_SPEED, this.speed + step)); | |
| } else if (cmd === "dur") { | |
| this.stopped = true; | |
| } else if (cmd === "ileri" || cmd === "geri") { | |
| const g = cmd === "ileri" ? 1 : -1; | |
| if (g !== this.gear || this.stopped) this.speed = CREEP; // vites değişince/dururken yavaştan başla | |
| this.gear = g; | |
| this.stopped = false; | |
| } else if (cmd === "sağa" || cmd === "sola") { | |
| this.lastDir = cmd === "sağa" ? 1 : -1; | |
| this.steerTarget = this.lastDir * MAX_STEER; | |
| } else if (cmd === "kır") { | |
| this.steerTarget = this.lastDir * MAX_STEER; | |
| } else if (cmd === "topla") { | |
| this.steerTarget = 0; | |
| } | |
| } | |
| update(dt) { | |
| const d = this.steerTarget - this.steer, r = STEER_RATE * dt; | |
| this.steer += Math.max(-r, Math.min(r, d)); | |
| if (this.stopped) this.speed = Math.max(0, this.speed - BRAKE * dt); | |
| const v = this.gear * this.speed; | |
| this.x += v * Math.cos(this.ang) * dt; | |
| this.y += v * Math.sin(this.ang) * dt; | |
| this.ang += (v / this.wb) * Math.tan(this.steer) * dt; | |
| } | |
| } | |
| function pick(rng, arr) { | |
| return arr[Math.floor(rng() * arr.length)]; | |
| } | |
| class Sim { | |
| // rng: test için tohumlanabilir rastgele sayı üreteci | |
| // opts.traffic: false ile yoldan geçen araçlar kapatılır (bot geometriyi trafiksiz sınar) | |
| // opts.avoid: [x0, y0, x1, y1] dikdörtgenleri (ekran butonları); hedef bunların altına düşmez | |
| // opts.row: "top" | "bottom" hedef sırasını zorlar (test için) | |
| constructor(worldName, levelIndex, rng = Math.random, opts = {}) { | |
| this.opts = opts; | |
| this.world = typeof worldName === "string" ? WORLDS[worldName] : worldName; // ad ya da {W, H, ...} | |
| this.level = LEVELS[levelIndex]; | |
| this.rng = rng; | |
| this.timeLeft = this.level.time; | |
| this.crashCool = 0; | |
| this.done = false; | |
| this.trafficOn = opts.traffic !== false; | |
| this.traffic = []; | |
| this.setup(); | |
| } | |
| setup() { | |
| const { W, H, CURB } = this.world, two = this.world.rows === 2, rowBottom = CURB + this.level.spotD; | |
| this.bottomCurb = two ? H - CURB : H; // alt kaldırımın başladığı y | |
| if (this.level.kind === "paralel") this.setupParalel(); | |
| else this.setupDik(); | |
| let startY; | |
| if (two) { | |
| // iki sıra: oyuncu üst sıranın altından başlar, iki yönlü şeritler yolun ortasında | |
| startY = this.level.kind === "paralel" ? rowBottom + 85 : rowBottom + 150; | |
| const mid = (startY + this.bottomCurb - this.level.spotD) / 2; | |
| this.lanes = this.level.kind === "paralel" ? [{ y: Math.max(startY + 62, mid), dir: 1 }] : [{ y: mid - 30, dir: 1 }, { y: mid + 30, dir: -1 }]; | |
| } else { | |
| // yolun sol alt kenarında park etmiş tır (sağ alt köşe ekran butonlarına ayrıldı); | |
| // oyuncu hep üstünden geçer, çizimde önde kalır | |
| const t = VEHICLES.tir; | |
| this.parked.push(this.vehicle("tir", t.L / 2 + 30, H - t.W / 2 - 8, Math.PI)); | |
| startY = this.level.kind === "paralel" ? rowBottom + 85 : rowBottom + 240; | |
| // şeritler: cep sırası ile oyuncunun başladığı yer arasında iki yön; paralelde oyuncunun bir altı | |
| this.lanes = this.level.kind === "paralel" ? [{ y: startY + 62, dir: 1 }] : [{ y: rowBottom + 110, dir: 1 }, { y: rowBottom + 170, dir: -1 }]; | |
| } | |
| this.car = new Car(60, startY, 0, pick(this.rng, CEP_TIPLERI), pick(this.rng, COLORS)); // sağa bakarak başlar | |
| for (const l of this.lanes) l.next = 1.5 + this.rng() * 4; // ilk araç biraz gecikmeli gelsin | |
| } | |
| spawnTraffic(dt) { | |
| const { W } = this.world; | |
| for (const l of this.lanes) { | |
| l.next -= dt; | |
| if (l.next > 0) continue; | |
| l.next = TRAFFIC.spawnMin + this.rng() * (TRAFFIC.spawnMax - TRAFFIC.spawnMin); | |
| if (this.traffic.filter((t) => t.lane === l).length >= TRAFFIC.perLane) continue; | |
| const type = pick(this.rng, CEP_TIPLERI), { L } = VEHICLES[type]; | |
| const x = l.dir > 0 ? -L / 2 - 10 : W + L / 2 + 10; | |
| const v = this.vehicle(type, x, l.y, l.dir > 0 ? 0 : Math.PI); | |
| // giriş ağzı doluysa (oyuncu ya da başka araç) bu sefer atla | |
| const probe = corners(x, l.y, v.ang, L + 120, v.W + 10); | |
| if (overlap(probe, this.car.poly()) || this.traffic.some((t) => overlap(probe, t.poly))) continue; | |
| const [lo, hi] = TRAFFIC.cruise, cruise = lo + this.rng() * (hi - lo); | |
| this.traffic.push(Object.assign(v, { lane: l, dir: l.dir, speed: cruise, cruise, braking: false, waited: 0, honked: false })); | |
| } | |
| } | |
| // Trafiği ilerletir; oyuncuya çarpma olursa (oyuncu önüne kırdıysa) true döner | |
| moveTraffic(dt, ev) { | |
| const { W } = this.world, player = this.car.poly(); | |
| let hit = false; | |
| for (const t of this.traffic) { | |
| // önündeki kutuda oyuncu ya da başka araç varsa dur | |
| const look = 20 + t.speed * 0.8, cx = t.x + t.dir * (t.L / 2 + TRAFFIC.minGap + look / 2); | |
| const ahead = corners(cx, t.y, t.ang, look, t.W + 14); | |
| const blocked = overlap(ahead, player) || this.traffic.some((o) => o !== t && overlap(ahead, o.poly)); | |
| t.braking = blocked; | |
| t.speed = blocked ? Math.max(0, t.speed - TRAFFIC.decel * dt) : Math.min(t.cruise, t.speed + TRAFFIC.accel * dt); | |
| if (blocked && t.speed === 0) { | |
| t.waited += dt; | |
| if (t.waited > TRAFFIC.honkAfter && !t.honked && overlap(ahead, player)) { | |
| t.honked = true; | |
| ev.push({ type: "honk" }); | |
| } | |
| } else if (!blocked) { | |
| t.waited = 0; | |
| t.honked = false; | |
| } | |
| const nx = t.x + t.dir * t.speed * dt, poly = corners(nx, t.y, t.ang, t.L, t.W); | |
| if (overlap(poly, player)) { | |
| t.speed = 0; // durmasına fırsat vermeden önüne kırıldı | |
| hit = true; | |
| continue; | |
| } | |
| t.x = nx; | |
| t.poly = poly; | |
| } | |
| this.traffic = this.traffic.filter((t) => t.x > -t.L - 40 && t.x < W + t.L + 40); | |
| return hit; | |
| } | |
| // Hedef: üst sırada uçlar hariç herhangi bir cep; iki sıralı dünyada yarı yarıya alt sıra da olabilir, | |
| // ama ekran butonlarının (opts.avoid) altında kalan alt cepler seçilmez | |
| pickTarget() { | |
| const ok = (sp, row) => { | |
| if (row[0] === sp || row[row.length - 1] === sp) return false; | |
| return !(this.opts.avoid || []).some(([x0, y0, x1, y1]) => sp.x < x1 && sp.x + sp.w > x0 && sp.y < y1 && sp.y + sp.h > y0); | |
| }; | |
| const rows = ["top", "bottom"].map((r) => this.spots.filter((sp) => sp.row === r)); | |
| const cands = rows.map((row) => row.filter((sp) => ok(sp, row))); | |
| let row = this.opts.row === "bottom" ? 1 : this.opts.row === "top" ? 0 : cands[1].length && this.rng() < 0.5 ? 1 : 0; | |
| if (!cands[row].length) row = 0; | |
| return this.spots.indexOf(pick(this.rng, cands[row])); | |
| } | |
| // Park etmiş araçlar biraz yamuk durur: yana kaymış, içeri/dışarı taşmış, hafif açılı | |
| parkIn(sp) { | |
| const r = this.rng, top = sp.row === "top"; | |
| const nose = r() < 0.75 ? 1 : -1; // çoğu burnu kaldırıma dönük, bazısı geri geri girmiş | |
| const ang = (top ? Math.PI / 2 : -Math.PI / 2) * nose + (r() - 0.5) * 0.2; | |
| const cx = sp.x + sp.w / 2 + (r() - 0.5) * 14; | |
| const cy = sp.y + sp.h / 2 + (top ? 1 : -1) * (r() - 0.35) * 14; | |
| return this.vehicle(pick(r, CEP_TIPLERI), cx, cy, ang); | |
| } | |
| setupDik() { | |
| const { W, CURB } = this.world, { spotW, spotD } = this.level; | |
| const nSpots = Math.min(this.world.nSpots, Math.floor((W - 20) / spotW)); | |
| const x0 = (W - nSpots * spotW) / 2; | |
| this.spots = Array.from({ length: nSpots }, (_, i) => ({ x: x0 + i * spotW, y: CURB, w: spotW, h: spotD, row: "top" })); | |
| if (this.world.rows === 2) { | |
| for (let i = 0; i < nSpots; i++) this.spots.push({ x: x0 + i * spotW, y: this.bottomCurb - spotD, w: spotW, h: spotD, row: "bottom" }); | |
| } | |
| this.target = this.pickTarget(); | |
| this.parked = []; | |
| this.spots.forEach((sp, i) => { | |
| if (i !== this.target) this.parked.push(this.parkIn(sp)); | |
| }); | |
| } | |
| // kaldırım boyunca dizili araçlar, aralarında bir boşluk; iki sıralı dünyada alt kaldırım da doludur | |
| setupParalel() { | |
| const { W, CURB } = this.world, { slotL, gapL, spotD } = this.level; | |
| const n = 1 + Math.floor((W - 40 - gapL) / slotL); | |
| const x0 = (W - (n - 1) * slotL - gapL) / 2; | |
| const ti = 1 + Math.floor(this.rng() * (n - 2)); | |
| this.spots = []; | |
| this.parked = []; | |
| const r = this.rng; | |
| for (let i = 0, x = x0; i < n; i++) { | |
| const w = i === ti ? gapL : slotL; | |
| this.spots.push({ x, y: CURB, w, h: spotD, row: "top" }); | |
| if (i !== ti) { | |
| const cx = x + w / 2 + (r() - 0.5) * 12, cy = CURB + spotD / 2 + (r() - 0.3) * 6; | |
| this.parked.push(this.vehicle(pick(r, CEP_TIPLERI), cx, cy, (r() - 0.5) * 0.1 + (r() < 0.3 ? Math.PI : 0))); | |
| } | |
| x += w; | |
| } | |
| this.target = ti; | |
| if (this.world.rows === 2) { | |
| // alt kaldırım: arka arkaya park etmiş, aralarında rastgele boşluklar | |
| for (let x = 20 + r() * 30; x + 90 < W - 10; x += 100 + r() * 40) { | |
| if (r() < 0.2) continue; | |
| const cy = this.bottomCurb - spotD / 2 - (r() - 0.3) * 6; | |
| this.parked.push(this.vehicle(pick(r, CEP_TIPLERI), x + 45, cy, Math.PI + (r() - 0.5) * 0.1 + (r() < 0.3 ? Math.PI : 0))); | |
| } | |
| } | |
| } | |
| vehicle(type, x, y, ang) { | |
| const { L, W, wb } = VEHICLES[type]; | |
| return { type, x, y, ang, L, W, wb, color: pick(this.rng, COLORS), poly: corners(x, y, ang, L, W) }; | |
| } | |
| collides(poly) { | |
| const { W, H, CURB } = this.world; | |
| if (poly.some(([x, y]) => x < 0 || x > W || y < CURB || y > this.bottomCurb)) return true; | |
| return this.parked.some((p) => overlap(poly, p.poly)) || this.traffic.some((t) => overlap(poly, t.poly)); | |
| } | |
| crash(ev, traffic) { | |
| if (this.crashCool > 0) return; | |
| this.crashCool = 1; | |
| ev.push({ type: "crash", points: -CRASH_PENALTY, traffic }); | |
| } | |
| isParked() { | |
| const s = this.spots[this.target], c = this.car; | |
| const inside = c.poly().every(([x, y]) => x >= s.x && x <= s.x + s.w && y >= s.y && y <= s.y + s.h); | |
| const tol = Math.cos((12 * Math.PI) / 180), kind = this.level.kind; | |
| const out = s.row === "bottom" ? -1 : 1; // geri parkta burun yola: üst sırada aşağı, alt sırada yukarı | |
| const straight = kind === "paralel" ? Math.abs(Math.cos(c.ang)) > tol : kind === "geri" ? out * Math.sin(c.ang) > tol : Math.abs(Math.sin(c.ang)) > tol; | |
| return inside && straight && c.stopped && c.speed < 1; | |
| } | |
| // dt saniye ilerletir; olayları döndürür: {type: "crash"} | {type: "park", points} | {type: "timeout"} | |
| step(dt) { | |
| if (this.done) return []; | |
| const ev = []; | |
| this.timeLeft = Math.max(0, this.timeLeft - dt); | |
| if (this.timeLeft <= 0) { | |
| this.done = true; | |
| return [{ type: "timeout" }]; | |
| } | |
| const c = this.car, prev = [c.x, c.y, c.ang]; | |
| this.crashCool -= dt; | |
| if (this.trafficOn) { | |
| this.spawnTraffic(dt); | |
| if (this.moveTraffic(dt, ev)) { | |
| c.speed = 0; | |
| c.stopped = true; | |
| this.crash(ev, true); | |
| } | |
| } | |
| c.update(dt); | |
| if (this.collides(c.poly())) { | |
| const poly = c.poly(), traffic = this.traffic.some((t) => overlap(poly, t.poly)); | |
| [c.x, c.y, c.ang] = prev; | |
| // biraz geri it ve durdur | |
| const back = -c.gear * 6; | |
| c.x += back * Math.cos(c.ang); | |
| c.y += back * Math.sin(c.ang); | |
| if (this.collides(c.poly())) [c.x, c.y, c.ang] = prev; | |
| c.speed = 0; | |
| c.stopped = true; | |
| this.crash(ev, traffic); | |
| } | |
| if (this.isParked()) { | |
| this.done = true; | |
| ev.push({ type: "park", points: (this.level.points || PARK_POINTS) + Math.floor(this.timeLeft) * TIME_BONUS }); | |
| } | |
| return ev; | |
| } | |
| } | |
| if (typeof module !== "undefined") { | |
| module.exports = { Sim, Car, LEVELS, WORLDS, VEHICLES, COLORS, MAX_STEER, corners, overlap }; | |
| } | |