atasoglu's picture
Claude Opus 5.5
Değnekçi Simulation v0.2.0
5fd40a4
Raw History Blame Contribute Delete
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 };
}