🤖 AI Fejezet
AI Fejezet · 1. Lecke

🤖 Miért más a játék AI mint a gépi tanulás?

A ChatGPT és az Alpha Zero is "mesterséges intelligencia" — mégis teljesen más elveken működnek. A játékprogramozásban a szabályalapú AI az egyértelmű nyerő: kiszámítható, debugolható, és percek alatt megírható.

⏱ 30 perc
🎯 Szabályalapú AI · Decision Making · Game Feel

1Az AI spektrum

TípusHogyan dönt?ElőnyHátrányPélda
Véletlenszerű Math.random() 1 sor kód Kiszámíthatatlan, nem érzi intelligensnek magát Minden kör véletlenszerű lépés
Szabályalapú If-else, súlyozott döntés Gyors, kiszámítható, debugolható Nem tanul, a tervező írja a szabályokat Teknős AI, Galactic AI, Castle ellenség
Fa alapú (BT/FSM) Állapotgép, döntési fa Komplex viselkedés, könnyen bővíthető Több tervezési munka RPG NPC, FPS ellenfél
Gépi tanulás Neurális háló, Q-learning Ember szintű vagy jobb Millió játékidő treningelés, óriási komplexitás AlphaGo, OpenAI Five
A játékprogramozó valósága

A legtöbb kereskedelmi játékban — beleértve az AAA címeket is — szabályalapú AI van. Nem mert a fejlesztők nem ismerik a gépi tanulást, hanem mert a szabályalapú AI kiszámítható, debugolható és szándékosan veszíthető. Egy játékos nem akar egy tökéletes AI ellen játszani — azt akar, ami szórakoztató.

2A jó játék-AI alapelvei

✅ AMIT AKARUNK
  • ▸ Legyen legyőzhető — de ne könnyű
  • ▸ Tegyen hibákat néha — emberibbnek hat
  • ▸ Reagáljon a játékos viselkedésére
  • ▸ Nehézségi szintenként eltérő
  • ▸ Gyors döntés (max pár ms/frame)
❌ AMIT NEM AKARUNK
  • ✗ Tökéletes döntés minden frame-ben
  • ✗ Látja az összes game state-et (cheating)
  • ✗ Azonnal reagál minden változásra
  • ✗ Előre kiszámítható mintát követ
  • ✗ Milliszekundum döntési idő
A három játékunk AI-jának összehasonlítása

Teknős Verseny — célpont kiválasztó AI: melyik tojás a legjobb? Időzített döntés, szándékos hiba.
Castle Siege — útvonalkövető AI: ellenségek fix sávon haladnak, hullámban érkeznek, nincs saját döntés.
Galactic Conquest — stratégiai AI: területértékelés, három akciótype, zajjal torzított döntés.

🧠 Miért nem tökéletes az AI a legtöbb játékban?

Mert a fejlesztők nem tudnak jobb AI-t írni
Mert egy tökéletes AI ellen játszani frusztráló és nem szórakoztató — a hibák teszik emberibbnek és legyőzhetővé
Mert a processzorok nem elég gyorsak
Mert a játék törvényei nem teszik lehetővé
AI Fejezet · 2. Lecke

🐢 Súlyozott célpontkiválasztás — Scoring rendszer

Az egyszerű "legközelebbi tojás" heurisztika nem elég. A valódi döntési rendszer egyszerre mérlegeli a közelséget, az energia-szükségletet, az akadályokat és a kockázatot — egyetlen pontszámban.

⏱ 60 perc
🎯 Weighted scoring · heurisztika · multi-faktor értékelés

1Az alap AI vs. a súlyozott AI

🐢 Teknős Verseny

Nézzük mi a különbség a jelenlegi egyszerű AI és a súlyozott döntéshozó között:

Egyszerű AI — csak közelségJavaScript
// ❌ EGYSZERŰ: csak a legközelebbi tojást keresi
G.objects.forEach(o => {
  if (o.type !== 'egg' || o.done) return;
  const sy = o.y + G.scrollY;
  if (sy > AI_Y - 60 && sy < AI_Y + 100) {
    const sc = 90 - Math.abs(o.x - a.x); // csak távolság!
    if (sc > bestSc) { bestSc = sc; best = o.x; }
  }
});

// Probléma: ha 2 tojás egyforma távolságra van,
// de az egyik mögött egy sirály van — az AI nem veszi észre
Súlyozott AI — több tényező egyszerreJavaScript
// ✅ SÚLYOZOTT: közelség + energia + kockázat együtt
function scoreTarget(obj, ai) {
  const sy  = obj.y + G.scrollY;
  const dx  = Math.abs(obj.x - ai.x);
  const dy  = Math.abs(sy - AI_Y);

  // 1. KÖZELSÉGI PONT: közelebb = jobb (max 100)
  const distScore = Math.max(0, 100 - dx * 0.8 - dy * 0.3);

  // 2. ÉRTÉKESSÉGI PONT: energia tárgy fontosabb ha alacsony az energia
  let valueScore = obj.type === 'egg' ? 50 : 0;
  if (obj.type === 'energy') {
    valueScore = ai.energy < 40 ? 120 : 20; // alacsony energiánál szuper értékes
  }

  // 3. KOCKÁZATI LEVONÁS: sirály közelben csökkenti az értéket
  let riskPenalty = 0;
  G.objects.forEach(threat => {
    if (threat.type !== 'gull' || threat.done) return;
    const gsy = threat.y + G.scrollY;
    const threatDist = Math.hypot(obj.x - threat.x, sy - gsy);
    if (threatDist < 60) riskPenalty += (60 - threatDist); // közel = büntetés
  });

  // Végső pontszám: mindhárom tényező összeadva
  return distScore + valueScore - riskPenalty;
}

// Felhasználás — a legjobb pontszámú célpont felé megy
let bestScore = -Infinity, bestX = W/2;
G.objects.forEach(o => {
  if (o.done) return;
  const sy = o.y + G.scrollY;
  if (sy < AI_Y + 120 && sy > AI_Y - 80) {
    const sc = scoreTarget(o, a);
    if (sc > bestScore) { bestScore = sc; bestX = o.x; }
  }
});
A súlyozás aranyszabálya

A különböző tényezők egységei nem összehasonlíthatók ("pixel" vs "energia%") — ezért normalizálni kell őket (0–100 skálára hozni), majd súlyozni. A súlyok meghatározása playtesting kérdése: próbáld ki, érezd, állítsd be.

2Teljes példa: a döntési pontszám kiegészítése felszínnel

Felszín alapú döntés hozzáadásaJavaScript
// 4. FELSZÍN BÓNUSZ: ha az objektum mögött jó felszín van
function surfaceBonus(targetX, currentDist) {
  // Megnézzük, hogy a cél felé vezető út milyen felszínen halad
  const surf = getSurface(G.totalDist + currentDist / 8, G.level);
  return surf === 'road' ? 30 : surf === 'mud' ? -20 : 0;
  // Úton: bónusz (gyorsabb lesz)  Sárban: büntetés (lelassul)
}

// Teljes scoreTarget kibővítve:
function scoreTarget(obj, ai) {
  const sy  = obj.y + G.scrollY;
  const dx  = Math.abs(obj.x - ai.x);

  const distScore   = Math.max(0, 100 - dx * 0.8);
  const valueScore  = obj.type === 'egg' ? 50 :
                       ai.energy < 40  ? 120 : 20;
  const riskPenalty = calcRisk(obj.x, sy);
  const surfBonus   = surfaceBonus(obj.x, dx);

  return distScore + valueScore - riskPenalty + surfBonus;
}
✏️ Feladat

Épits egy egyszerű scoring rendszert:

  • Nyisd meg a turtle_race.html-t, keresd meg az updateAI() függvényt
  • Egészítsd ki az sc pontszámot: ha az AI energia < 30%, az energia tárgyak kapjanak +80 bónuszt
  • Add hozzá a kockázatot: ha bármely sirály 50px-en belül van a célponttól, vonj le 40 pontot
  • Figyeld meg F12-vel: console.log(bestScore) — mikor milyen értékek jönnek?

🧠 Miért kell normalizálni a különböző tényezőket (0–100 skálára)?

Mert a JavaScript nem tud 100-nál nagyobb számokat összeadni
Mert különböző mértékegységű értékeket (pixel távolság, energia%, kockázati szint) csak akkor lehet értelmesen összehasonlítani és súlyozni, ha azonos skálára hozzuk őket
Hogy az AI gyorsabb legyen
Nincs különösebb oka, megállapodás kérdése
AI Fejezet · 3. Lecke

🐢 AI Állapotgép — Amikor az AI is "érez"

Egy jó AI nem ugyanúgy viselkedik minden helyzetben. Ha kevés az energiája, menekül a tojások után. Ha vezet, óvatosabb. Ha éppen ugrik, más döntéseket hoz. Ez az állapotgép.

⏱ 75 perc
🎯 FSM · AI states · context-aware behavior

1Az AI állapotai — FSM tervezés

🏃 HUNTING
Tojást keres, normál pontozással.
Feltétel: energia > 40%
⚡ LOW ENERGY
Csak energia tárgyat keres, tojást figyelmen kívül hagy.
Feltétel: energia < 40%
🛡️ DEFENSIVE
Pálya közepén marad, kerüli a széleket.
Feltétel: életei < 2
turtle_race.html — AI állapotgép implementációjaJavaScript
// AI állapotok
const AI_STATES = {
  HUNTING:    'hunting',
  LOW_ENERGY: 'low_energy',
  DEFENSIVE:  'defensive',
};

// Az AI rendelkezik saját állapot-változóval
const ai = {
  x: W/2, vx: 0,
  energy: 100, lives: 3,
  state:  AI_STATES.HUNTING,  // kezdőállapot
  aiTimer: 0, aiTargetX: W/2,
};

// ─── ÁLLAPOT FRISSÍTÉSE ─────────────────────────────────────
function updateAIState() {
  if (ai.lives <= 1) {
    ai.state = AI_STATES.DEFENSIVE;  // kevés élet → védekező
  } else if (ai.energy < 40) {
    ai.state = AI_STATES.LOW_ENERGY; // alacsony energia → energia után fut
  } else {
    ai.state = AI_STATES.HUNTING;    // normál → tojást keres
  }
}

// ─── AI DÖNTÉS AZ ÁLLAPOT ALAPJÁN ──────────────────────────
function updateAI() {
  updateAIState(); // minden frame-ben frissítjük

  ai.aiTimer--;
  if (ai.aiTimer > 0) return; // még nem döntési idő
  ai.aiTimer = 25 + Math.floor(Math.random() * 40);

  switch (ai.state) {

    case AI_STATES.LOW_ENERGY:
      // Csak energiát keres — tojást figyelmen kívül hagy
      ai.aiTargetX = findBest(['energy'], { energyWeight: 200, distWeight: 1 });
      break;

    case AI_STATES.DEFENSIVE:
      // Középen marad — alacsony kockázat
      ai.aiTargetX = W / 2 + (Math.random() - .5) * 60;
      break;

    case AI_STATES.HUNTING:
    default:
      // Normál scoring — tojás + energia + kockázat
      ai.aiTargetX = findBest(['egg','energy'], { energyWeight: 60, distWeight: 1 });
      break;
  }
}

// findBest: keresés a megadott típusok között, súlyozással
function findBest(types, weights) {
  let bestSc = -Infinity, bestX = W/2;
  G.objects.forEach(o => {
    if (!types.includes(o.type) || o.done) return;
    const sy = o.y + G.scrollY;
    if (sy < AI_Y + 120 && sy > AI_Y - 80) {
      const sc = scoreTarget(o, ai, weights);
      if (sc > bestSc) { bestSc = sc; bestX = o.x; }
    }
  });
  return bestX + ai.aiErr * TW * .4;
}
Miért switch és nem if-else lánc?

Ha sok állapot van, a switch átláthatóbb — minden case egy önálló "viselkedés modul". Ha később új állapotot kell hozzáadni (pl. AGGRESSIVE ha vezet), csak egy case blokkot kell felvenni, a többi kódot nem kell bolygatni.

✏️ Feladat

Adj hozzá egy negyedik állapotot: AGGRESSIVE

  • Az AI "agresszív" legyen ha 10+ tojással vezet a játékos előtt
  • Agresszív módban: az aiTimer legyen feleakkora (gyorsabban dönt)
  • Agresszív módban: az aiErr értéke legyen 0 (tökéletesebb célzás)
  • Debugold: console.log(ai.state) minden döntésnél — mikor vált állapotot?

🧠 Mikor kerül az AI LOW_ENERGY állapotba az implementációnk szerint?

Ha az AI kevesebb tojást gyűjtött mint a játékos
Ha az AI energia értéke 40% alá esik
Ha az AI egynél kevesebb életet veszített
Ha az AI nem talál energiát 5 másodpercig
AI Fejezet · 4. Lecke

🎮 AI Nehézségi szintek — Egy AI, három viselkedés

Nem kell külön AI-t írni minden nehézségi szinthez. Egyetlen paraméter-készlet megváltoztatásával — reakcióidő, hibamérték, látótávolság — az AI könnyűtől a nagyon nehézig skálázható.

⏱ 45 perc
🎯 difficulty config · parameter tuning · playtesting

1A paraméter-alapú nehézség

Minden AI-t leíró paraméter befolyásolható a nehézségi szinttel. A kulcs az, hogy ezek a paraméterek egy konfigurációs objektumban legyenek, nem szórva a kódban:

😊 Könnyű
Reakcióidő45–85 frame
Hibamérték (aiErr)±0.5
Látótávolság80 px
Ugráskészség50%
Kockázatérzékenységalacsony
😐 Közepes
Reakcióidő25–55 frame
Hibamérték (aiErr)±0.25
Látótávolság120 px
Ugráskészség80%
Kockázatérzékenységközepes
😈 Nehéz
Reakcióidő8–20 frame
Hibamérték (aiErr)±0.05
Látótávolság180 px
Ugráskészség98%
Kockázatérzékenységmagas
AI nehézségi config — a Teknős VersenyhözJavaScript
// Konfigurációs tömb — minden szintnek saját paraméterei
const AI_DIFF = [
  { // 0 = KÖNNYŰ
    name:       'Könnyű',
    timerMin:   45,  timerRnd:  40,  // 45–85 frame döntési késés
    errScale:   0.5,               // nagy hiba — gyakran mellé megy
    sight:      80,                // csak 80px-en belüli tárgyakat lát
    jumpChance: 0.5,              // 50% esély hogy kő fölött ugrik
    riskWeight: 0.3,              // nem menekül annyira a sirályoktól
  },
  { // 1 = KÖZEPES
    name:       'Közepes',
    timerMin:   25,  timerRnd:  30,
    errScale:   0.25,
    sight:      120,
    jumpChance: 0.8,
    riskWeight: 0.7,
  },
  { // 2 = NEHÉZ
    name:       'Nehéz',
    timerMin:   8,   timerRnd:  12,
    errScale:   0.05,
    sight:      180,
    jumpChance: 0.98,
    riskWeight: 1.2,
  },
];

let DIFF = 1; // jelenlegi nehézség

// AI inicializálása a nehézség alapján
function initAI() {
  const dc = AI_DIFF[DIFF];
  ai.aiErr = (Math.random() - .5) * dc.errScale; // állandó hibamérték
  ai.aiTimer = dc.timerMin;
}

// updateAI-ban: a config alapján dönt
function updateAI() {
  const dc = AI_DIFF[DIFF]; // aktuális config
  ai.aiTimer--;
  if (ai.aiTimer > 0) return;
  ai.aiTimer = dc.timerMin + Math.floor(Math.random() * dc.timerRnd);

  // Látótávolság: nehézebben csak közelebb lévőket lát
  const sightRange = dc.sight;
  G.objects.forEach(o => {
    const sy = o.y + G.scrollY;
    if (sy < AI_Y + sightRange && sy > AI_Y - sightRange/2) {
      // ... scoring ...
    }
  });

  // Ugráskészség: könnyűn nem mindig ugrik kő fölött
  G.objects.forEach(o => {
    if (o.type !== 'rock') return;
    const sy = o.y + G.scrollY;
    if (sy > AI_Y-60 && sy < AI_Y+20 && Math.abs(o.x-ai.x) < 35) {
      if (Math.random() < dc.jumpChance) { // nem mindig ugrik!
        ai.jumping = true; ai.jumpT = 0;
      }
    }
  });
}
✏️ Feladat

Adj hozzá nehézségválasztó gombot a játékodhoz:

  • Csinálj 3 HTML gombot: "😊 Könnyű", "😐 Közepes", "😈 Nehéz"
  • Kattintásra állítsd be a DIFF változót (0, 1, vagy 2)
  • Hívd meg az initAI()-t nehézségváltáskor
  • Teszteld: érezhetően különbözik a három szint viselkedése?

🧠 Mit jelent az aiErr értéke az AI-ban?

Hány hibát vétett az AI összesen
Egy állandó, véletlenszerű eltolás (offset) amellyel az AI szisztematikusan kicsit jobbra vagy balra "téved" — ez teszi emberibbé a mozgást
Az AI hibás döntéseinek aránya (0–1 között)
A döntési időköz hibahatára
AI Fejezet · 5. Lecke

🏰 Castle Siege — Ellenség útvonalkövetés

A Castle Siege ellenségei nem "gondolkodnak" — fix célpont felé tartanak a legegyszerűbb lehetséges módon. Ez az úgynevezett lineáris pályakövetés. Mégis érdekesen hat, mert a kőzárak, a várárok és a sebességkülönbségek változatosságot adnak.

⏱ 60 perc
🎯 Lineáris mozgás · sebességskálázás · akadálykezelés

1A Castle Siege ellenség mozgáslogikája

🏰 Castle Siege

Az ellenség mozgása meglepően egyszerű: mindig a kastély felé (bal) haladnak, és automatikusan középre igazítják az Y pozíciójukat (MIDY). Nincs útvonaltérkép, nincs akadály-kerülés — mégis hatásos.

castle_siege.html — updEnemies() mozgás logikaJavaScript
function updEnemies() {
  for (let i = G.enemies.length-1; i >= 0; i--) {
    const e = G.enemies[i];
    const et = ET[e.ti]; // az ellenség típusa (Goblin/Ork/Troll)

    // ─── KASTÉLY KAPUNÁL: TÁMADÁS ───────────────────────────
    if (e.x - et.r <= CW) {  // elérte a kastélyfalat
      e.atkCD -= DT;
      if (e.atkCD <= 0) {
        const dmg = Math.round(et.dmg * e.dmM);
        G.castle.hp = Math.max(0, G.castle.hp - dmg);
        e.atkCD = 80;
      }
    }

    // ─── HALADÁS: JOBBRÓL BALRA ─────────────────────────────
    else {
      const sp = eSpdOf(e); // sebesség: típus × felszín módosítók
      e.x -= sp * DT;        // mindig balra megy (kastély felé)

      // Y tengely: középre igazítás — nem egyenesen halad, hanem feléje húz
      const diff = MIDY - e.y;
      e.vy = Math.sign(diff) * Math.min(Math.abs(diff) * .008, .5);
      e.y += e.vy * DT;
      // Ez adja a természetes csoportosulást: mindenki a pálya közepére tart
    }
  }
}

// ─── SEBESSÉG KISZÁMÍTÁSA (FELSZÍN + PÁLYAELEMEK) ─────────
function eSpdOf(e) {
  const et = ET[e.ti];
  let spd = et.spd * e.spM; // alap sebesség × hullám szorzó

  // Kőzárban: 35%-ra lassul (ha kőzár van a pályán)
  G.pits.forEach(p => {
    if (dist(e.x, e.y, p.x, p.y) < s(52)) spd *= .35;
  });

  // Várárokba: 70%-ra lassul (szintfüggő)
  if (SV.level >= 15 && e.x >= CW && e.x <= CW + MOAT_W) spd *= .7;

  return spd;
}
A .008 szorzó titka — spring/rubber band mozgás

A diff * 0.008 képlet egy "rugó" hatást kelt: minél messzebb van az ellenség a középvonaltól, annál gyorsabban húzza vissza — de soha nem egyenesen, mindig simán. Ez a spring dampening — rengeteg játékban alkalmazzák mozgásra, kamerakövetésre, AI mozgásra egyaránt.

2Ellenség típusok — különböző viselkedés ugyanazon logikával

castle_siege.html — ET (Enemy Types)JavaScript
// Egy tömb, három különböző "AI" viselkedés — mind ugyanaz a mozgáskód!
const ET = [
  // Gyors, gyenge — tömegesen jön, nehéz eltalálni
  { name: 'Goblin', hp: 38,  spd: s(1.3),  dmg: 10, gold: 10, r: s(13), col: '#2e7a2e' },
  // Közepes — kiegyensúlyozott
  { name: 'Ork',    hp: 100, spd: s(0.75), dmg: 22, gold: 28, r: s(18), col: '#8B4513' },
  // Lassú, hatalmas HP — toronyágyúkkal nehéz leállítani
  { name: 'Troll',  hp: 280, spd: s(0.38), dmg: 45, gold: 65, r: s(26), col: '#4a5a2a' },
];

// A hullámszorzók (hpM, spM) scale-elik ezeket pályánként:
// 15. pályán egy Goblin: hp=38×3.5=133, spd=1.3×1.8=2.34
// Ugyanaz a kód fut, de sokkal erősebb az ellenség
TípusStratégiaEllene hatékonyEllene kevésbé hatékony
🟢 GoblinSebesség — átrohan mielőtt a torony sokat lőGyors tüzelésű torony, hősErős de lassú torony
🟤 OrkKiegyensúlyozott — semmiben nem extremumMinden normál torony
🟫 TrollHP-tank — felszívja a tornyok lövedékeitNagy sebzésű torony, kőzár (lassít)Gyors, kis sebzésű torony
✏️ Feladat

Adj hozzá egy 4. ellenségtípust a Castle Siege-hez:

  • Hozz létre egy "Árnyék Assassin" típust: nagyon gyors (spd: 2.5), nagyon kevés HP (hp: 15), átrepül a kőzárak fölött (nem lassul a piteken)
  • Az eSpdOf() függvényben add hozzá: ha e.ti === 3, skip a pit ellenőrzést
  • Adj hozzá egy hullámba az új típust a genLevel()-ben (pl. 12. pályától)
  • Teszteld: kijön-e az Assassin a várárokból gyorsabban?

🧠 Mit csinál a Math.sign(diff) * Math.min(Math.abs(diff) * .008, .5) képlet?

Véletlenszerű mozgást generál a Y tengelyen
Rugószerű középre-húzást valósít meg: a MIDY-től való távolsággal arányos erő húzza vissza az ellenséget, de max 0.5 sebességgel
Egyenes vonalú mozgást számít ki középfelé
Az ellenség Y koordinátáját normalizálja
AI Fejezet · 6. Lecke

🌊 Castle Siege — Hullám-rendszer és ellenség-spawning

Az ellenségek nem egyszerre jönnek — hullámokban érkeznek, pályánként más összetételben. A genLevel() egyetlen matematikai formulával generálja mind az 50 pálya összes hullámát.

⏱ 60 perc
🎯 Wave generation · spawn queue · difficulty scaling

1A genLevel() — 50 pálya egyetlen függvényből

🏰 Castle Siege
castle_siege.html — genLevel(li)JavaScript
function genLevel(li) {
  // Hullámszám: 1-14. pályán 5, 15-21. pályán 7, aztán max 20
  const wc = li < 15 ? 5 : li < 21 ? 7 : Math.min(7 + (li-21), 20);

  const waves = [];
  for (let wi = 0; wi < wc; wi++) {

    // HP szorzó: pályánként 10%, hullámonként 22% nő
    const hpM = (1 + li * .10) * (1 + wi * .22);
    // Sebesség szorzó: lassabban nő
    const spM = (1 + li * .05) * (1 + wi * .08);
    // Sebzés szorzó: HP gyöke (nem nő olyan gyorsan)
    const dmM = Math.sqrt(hpM);

    // Ellenségek száma: pályával és hullámmal nő
    const base = 3 + Math.floor(li * .6) + Math.floor(wi * 1.5);

    // Csoportok: pályánkként új típus jelenik meg
    const groups = [{ ti: 0, n: base }]; // Goblinok mindig vannak
    if (li >= 3) groups.push({ ti: 1, n: Math.max(1, Math.floor((li-2) * .4 + wi * .9)) });
    if (li >= 8) groups.push({ ti: 2, n: Math.max(1, Math.floor((li-7) * .22 + wi * .4)) });
    // 3. pályától Orkok is // 8. pályától Trollok is

    // Spawn időköz: pályánként gyorsabb (minimum 14 frame)
    const ivl = Math.max(14, 56 - li * .5);

    waves.push({ groups, hpM, spM, dmM, ivl });
  }
  return waves;
}
Procedurális nehézségnövelés — miért elegáns?

Ahelyett hogy 50 pálya adatát kézzel beírnánk, a matematika csinálja. A 1 + pálya * 0.10 szorzó azt jelenti: minden pályán 10%-kal erősebbek az ellenségek. Az 50. pályán: 1 + 50 * 0.10 = 6.0 — a Goblin alap HP-ja (38) 228-ra nő. Egy képlet, végtelen változatosság.

2A Spawn Queue — sorban érkeznek

castle_siege.html — hullám spawn logikaJavaScript
// Új hullám indítása
function advWave() {
  G.waveIdx++;
  if (G.waveIdx >= G.waves.length) { lvlDone(); return; }

  G.phase = 'spawning';
  const wd = G.waves[G.waveIdx];

  // 1. Feltöltjük a spawn sort (queue) — az összes csoport elemeit
  G.spawnQ = [];
  wd.groups.forEach(g => {
    for (let i = 0; i < g.n; i++) {
      G.spawnQ.push({ ti: g.ti, hpM: wd.hpM, spM: wd.spM, dmM: wd.dmM });
    }
  });

  // 2. Megkeverjük — nem mindig ugyanolyan sorrendben jönnek
  for (let i = G.spawnQ.length-1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i+1));
    [G.spawnQ[i], G.spawnQ[j]] = [G.spawnQ[j], G.spawnQ[i]]; // swap
  }

  G.spawnT = wd.ivl; // időzítő az első ellenségig
}

// Update loop: minden ivl frame-ben kivesz egyet a sorból
if (G.phase === 'spawning') {
  G.spawnT -= DT;
  if (G.spawnT <= 0) {
    if (G.spawnQ.length > 0) {
      spawnE(G.spawnQ.shift());  // shift = kiveszi az első elemet
      G.spawnT = G.waves[G.waveIdx].ivl; // újraindítja az időzítőt
    } else {
      G.phase = 'fighting'; // mindenki spawn'd → harc fázis
    }
  }
}
Queue (sor) adatstruktúra — FIFO

A spawn sor egy FIFO (First In, First Out) lista: amit először beletettünk, azt vesszük ki először. JavaScriptben egy tömb push() + shift() kombinációval valósítja meg. A shift() kiveszi és visszaadja az első elemet — O(n) lassú nagy tömböknél, de itt max ~30 elem van egyszerre.

✏️ Feladat

Módosítsd a hullámrendszert:

  • Keresd meg a genLevel() függvényt — mi a hullámok száma a 10. pályán? (wc értéke)
  • Adj hozzá egy "Boss" hullámot minden pálya végén: 1 Troll, de 5× annyi HP-val
  • Implementáld: az utolsó hullámnál (wi === wc-1) a groups csak egy Trollból álljon, hpM × 5-tel
  • Teszteld: érzékel-e különbséget a játék nehézségén?

🧠 Miért keverjük meg véletlenszerűen a spawn sort (spawnQ)?

Mert a JavaScript tömbök alapból nem tartják a sorrendet
Hogy ne legyen előre kiszámítható a sorrend — ha mindig Goblin→Ork→Troll sorrendben jönnének, a játékos pontosan tudná mikor jön a Troll. A véletlenszerűség kiszámíthatatlanná és érdekesebbé teszi
Mert a forEach nem garantál sorrendet
Technikai okból: a push() nem garantál rendezett sorrendű kivételt
AI Fejezet · 7. Lecke

🚀 Galactic Conquest — Hex-grid AI alapok

A Galactic Conquest körös stratégiai játékban az AI hat szomszédos hexagont értékel körönként, és háromféle akciót hajthat végre: terjeszkedés, támadás, erősítés. Az egyszerű szabályok komplex stratégiát eredményeznek.

⏱ 90 perc
🎯 hex koordináták · szomszéd-keresés · aiTurn()

1A hex-grid koordináta rendszer

🚀 Galactic Conquest
galactic_conquest.html — hex szomszédokJavaScript
// Egy hexagon szomszédjainak megkeresése
// Offset koordinátákban (nem axialis) a páros és páratlan sorok eltolódnak
function hexNeighbors(col, row) {
  // Hex középpontja pixelekben
  const { x: cx, y: cy } = hexCenter(col, row);
  const nbrs = [];

  // Végigmegyünk az összes hexagonon — szomszédos ha közel elég
  G.hexes.forEach(h => {
    if (h.col === col && h.row === row) return; // önmaga nem szomszéd
    const { x, y } = hexCenter(h.col, h.row);
    const d = Math.hypot(x - cx, y - cy); // pitagorasz távolság
    if (d < HEX_R * 2.1) nbrs.push([h.col, h.row]); // közelebb mint 2.1 × sugár
  });

  return nbrs; // [[col1,row1], [col2,row2], ...] (általában 6 szomszéd)
}

// Egy szomszéd megszerzése: getHex(col, row)
function getHex(col, row) {
  return G.hexes.find(h => h.col === col && h.row === row);
}

// Segédfüggvények az AI-hoz
function playerHexes() { return G.hexes.filter(h => h.owner === 'p'); }
function aiHexes()     { return G.hexes.filter(h => h.owner === 'a'); }
function totalStr(owner) {
  return G.hexes
    .filter(h => h.owner === owner)
    .reduce((s, h) => s + h.str, 0); // összes erő összege
}
Miért pixeltávolsággal keressük a szomszédokat?

Hexagonális rácsoknál az axialis koordináták (q, r, s) matematikailag elegansebbek, de pixel-távolsággal is helyesen működik: ha két hex középpontja közelebb van mint 2.1 × sugár, szomszédok. A 2.1-es szorzó kis toleranciát ad a lebegőpontos kerekítési hibákra.

2Az AI köre — kandidáns-lista építése

galactic_conquest.html — aiTurn() kandidáns listaJavaScript
function aiTurn() {
  if (G.phase !== 'ai') return;

  // Energia bevétel: nehézségen alapszik
  const dc = DIFF_CFG[DIFF];
  const income = energyIncome('a');
  G.aEnergy = Math.min(G.aEnergy + Math.ceil(income * dc.aiEnergy), 99);

  // Könnyűn 3 akció/kör, nehézen 4
  let actions = 3 + (DIFF === 2 ? 1 : 0);

  for (let act = 0; act < actions; act++) {
    const myHexes = aiHexes();
    if (myHexes.length === 0) break;

    // ─── KANDIDÁNS LISTA ÉPÍTÉSE ────────────────────────────
    let moves = [];

    myHexes.forEach(h => {
      hexNeighbors(h.col, h.row).forEach(([c, r]) => {
        const t = getHex(c, r);
        if (!t) return;

        // Játékos hexje → TÁMADÁS jelölt
        if (t.owner === 'p')
          moves.push({ type: 'attack', from: h, to: t,
            score: t.str + STYPES[t.type].energy }); // gyenge + értékes = jobb célpont

        // Üres hex → TERJESZKEDÉS jelölt (ha van energia)
        if (t.owner === null && G.aEnergy >= 3)
          moves.push({ type: 'expand', from: h, to: t,
            score: STYPES[t.type].energy + .5 }); // értékesebb szektor = jobb
      });

      // Gyenge saját hex → ERŐSÍTÉS jelölt
      if (h.str < 3 && G.aEnergy >= 5)
        moves.push({ type: 'reinforce', from: h, to: h, score: 2 });
    });

    if (moves.length === 0) break; // nincs lehetséges lépés
    // ... rendezés és végrehajtás a következő leckében ...
  }
}
A három akciótípus és pontszámaik

attack score = t.str + t.energy — a gyenge (kis str) és értékes (nagy energy) hexek a legjobb célpontok.
expand score = t.energy + 0.5 — az energiatermelőbb szektorok hasznosabbak.
reinforce score = 2 — rögzített érték, kevésbé fontos mint az offenzív lépések.

✏️ Feladat

Elemezd a Galactic Conquest AI logikáját:

  • Nyisd meg a galactic_conquest.html-t, keresd meg az aiTurn() függvényt
  • Adj hozzá egy console.log-ot: mennyi kandidáns lépés volt az adott körben?
  • Módosítsd az erősítés (reinforce) score-ját 2-ről 10-re — mikor erősít az AI?
  • Figyelj meg 5 AI kört: mindig ugyanazt a lépéstípust választja-e?

🧠 Miért pont t.str + STYPES[t.type].energy az attack score?

Véletlenszerű — bármilyen képlet megtette volna
Mert az AI a legértékesebb és legkönnyebben bevehető hexeket preferálja egyszerre: a kis str könnyű célpont, a nagy energy értékes nyeremény — a kettő összeadva egy jó heurisztika
Mert ez az egyetlen matematikailag korrekt értékelés
Technikai korlátból: más érték nem fér el a float-ban
AI Fejezet · 8. Lecke

🚀 Galactic Conquest — Legjobb lépés kiválasztása és végrehajtása

A kandidáns lista megvan. Most rendezni kell pontszám szerint, kiválasztani a legjobbat, és végrehajtani a harcot — véletlenszerű kockadobással. A harc kimenetele soha nem biztos.

⏱ 60 perc
🎯 sort + noise · combat resolution · akció végrehajtás

1Rendezés és legjobb lépés kiválasztása

🚀 Galactic Conquest
galactic_conquest.html — rendezés és végrehajtásJavaScript
// ─── RENDEZÉS: score + zaj alapján ──────────────────────────
// A noise (zaj) teszi kiszámíthatatlanná — könnyűn nagy, nehézen kicsi
moves.sort((a, b) => {
  const noise = DIFF === 0 ? .8 : DIFF === 1 ? .3 : .1;
  return (b.score + Math.random() * noise) - (a.score + Math.random() * noise);
});

// Legjobb lépés: a rendezés utáni első elem
const mv = moves[0];

// ─── VÉGREHAJTÁS: az akció típusától függ ───────────────────
if (mv.type === 'attack') {
  // Harc: AI erő × nehézségi bónusz + véletlen vs. védő erő + véletlen
  const atk = mv.from.str * dc.aiBonus + Math.ceil(Math.random() * 3);
  const def = mv.to.str + Math.ceil(Math.random() * 2);

  if (atk > def) {
    // GYŐZELEM: átveszi az irányítást
    mv.to.owner = 'a';
    mv.to.str = Math.max(1, mv.from.str - 1); // a harc gyengíti a győztest is
    mv.to.flash = 18; // vizuális visszajelzés
  } else {
    // VERESÉG: a támadó hex gyengül
    mv.from.str = Math.max(1, mv.from.str - 1);
  }

} else if (mv.type === 'expand') {
  // TERJESZKEDÉS: 3 energia árán elfoglalja az üres hexet
  G.aEnergy -= 3;
  mv.to.owner = 'a'; mv.to.str = 1;

} else if (mv.type === 'reinforce') {
  // ERŐSÍTÉS: 5 energia árán +2 erő
  G.aEnergy -= 5; mv.from.str += 2;
}
Miért nem a legjobb lépést hajtja végre mindig?

A Math.random() * noise hozzáadása a pontszámhoz azt jelenti, hogy néha az AI a 2. vagy 3. legjobb lépést is választja. Könnyűn (noise=0.8) szinte bármit választ, nehézen (noise=0.1) szinte mindig a legjobbat. Ez az egyetlen sor teszi a három nehézségi szintet valóban különbözővé.

2A harc matematikája — miért van mindig kockázat?

Harc valószínűség — könnyen számíthatóJavaScript
// AI támad: str=4, aiBonus=1.4 (nehéz), véletlen 1-3
// Atk minimum: 4×1.4 + 1 = 6.6  maximum: 4×1.4 + 3 = 8.6

// Védő str=3, véletlen 1-2
// Def minimum: 3+1 = 4  maximum: 3+2 = 5

// Győzelem esélye: szinte biztos (atk min 6.6 > def max 5)
// De! Ha az AI str=1 és a védő str=3:
// Atk maximum: 1×1.4 + 3 = 4.4  Def minimum: 3+1 = 4
// → Győzelem esélye: ~10% — az AI ritkán támad ilyen hexet

// Ezért a score = t.str + t.energy heurisztika hasznos:
// kis str → könnyebben bevehető → magasabb score → AI előnyben részesíti
const atk = mv.from.str * dc.aiBonus + Math.ceil(Math.random() * 3);
const def = mv.to.str                + Math.ceil(Math.random() * 2);
const win = atk > def; // egyszerű összehasonlítás — de véletlen miatt sosem biztos
✏️ Feladat

Módosítsd a harc mechanikáját:

  • Adj hozzá egy "flankálás bónuszt": ha az AI-nak 2 szomszédja van a célpontnak, az atk + 1 bónuszt kap
  • Implementáld: const flanks = hexNeighbors(mv.to.col, mv.to.row).filter(([c,r]) => getHex(c,r)?.owner === 'a').length
  • Adj hozzá: const atk = mv.from.str * dc.aiBonus + Math.ceil(Math.random()*3) + (flanks >= 2 ? 1 : 0)
  • Figyelj meg 10 kört — érezhetően agresszívebb lett az AI?

🧠 Mit ér el a Math.random() * noise hozzáadása a rendezési feltételhez?

Meggyorsítja a rendezési algoritmust
Kiszámíthatatlanná teszi a lépésválasztást: a pontszámhoz adott véletlen zaj miatt néha nem a legjobb lépést választja az AI — könnyűn nagy a zaj (szinte véletlenszerű), nehézen kicsi (szinte mindig optimális)
Normalizálja a pontszámokat 0 és 1 közé
Megakadályozza a döntetlen eseteket a rendezésnél
AI Fejezet · 9. Lecke

🎯 Galactic Conquest — Stratégiai mélység és nehézség finomhangolása

A DIFF_CFG tömb és a noise paraméter az AI teljes viselkedését meghatározza. De mit jelent valójában a "stratégiai mélység" — és hogyan bővíthető az AI további taktikákkal?

⏱ 60 perc
🎯 DIFF_CFG · stratégiai bővítés · defensive AI

1A DIFF_CFG részletesen

🚀 Galactic Conquest
galactic_conquest.html — DIFF_CFGJavaScript
const DIFF_CFG = [
  { name: 'KÖNNYŰ',
    aiBonus:  .8,   // AI harcereje GYENGÉBB (0.8× szorzó)
    aiEnergy: 1.0, // AI energiabevétele: ugyanannyi mint a játékosé
  },
  { name: 'KÖZEPES',
    aiBonus:  1.1, // AI kicsit ERŐSEBB (+10%)
    aiEnergy: 1.2, // AI 20%-kal több energiát kap körönként
  },
  { name: 'NEHÉZ',
    aiBonus:  1.4, // AI jelentősen ERŐSEBB (+40%)
    aiEnergy: 1.5, // AI 50%-kal több energiát kap — gyorsabban terjeszkedik
  },
];

// Nehézen az AI közepes str hexekkel is tud nyerni:
// atk = 3 × 1.4 = 4.2, def max = 4 → szinte biztos győzelem
// Könnyűn str=3 AI: atk max = 3 × 0.8 + 3 = 5.4, def min = 1+1=2 → nem mindig nyer
Resource advantage vs. skill advantage

Az aiEnergy szorzó a "cheating" módszer: az AI több erőforrást kap. Az aiBonus a "skill" módszer: az AI hatékonyabban harcol. A jó nehézségi tervezés mindkettőt adagolva kombinálja — nehézen az AI nemcsak erősebb, hanem "okosabb" is (kisebb zaj, több erőforrás).

2Az AI bővítése — védekező stratégia

Védekező AI kiegészítés — HQ védelmeJavaScript
// A jelenlegi AI nem védi prioritással a saját HQ-ját
// Bővítés: ha a HQ veszélyben van, az erősítés prioritást kap

function aiTurnExtended() {
  const dc = DIFF_CFG[DIFF];
  const myHexes = aiHexes();

  // ─── VÉSZHELYZET: HQ veszélyben van? ───────────────────
  const hq = myHexes.find(h => h.col === COLS-1 && h.row === ROWS-1);
  if (hq) {
    const hqNeighbors = hexNeighbors(hq.col, hq.row);
    const enemyNearHQ = hqNeighbors.filter(([c,r]) => getHex(c,r)?.owner === 'p').length;
    if (enemyNearHQ > 0 && hq.str < 4 && G.aEnergy >= 5) {
      // HQ veszélyben: azonnal erősít, más lépés előtt
      G.aEnergy -= 5; hq.str += 2;
      addLog('🤖 AI védi a bázist!', 'a');
    }
  }

  // ─── TERJESZKEDÉSI PRIORITÁS: értékes szektorok ────────
  const bestExpand = myHexes
    .flatMap(h => hexNeighbors(h.col, h.row)
      .map(([c,r]) => ({ hex: getHex(c,r), from: h })))
    .filter(m => m.hex?.owner === null && G.aEnergy >= 3)
    .sort((a,b) => STYPES[b.hex.type].energy - STYPES[a.hex.type].energy)[0];
    // Rendezve: legértékesebb üres hex kerül az elejére

  if (bestExpand) {
    G.aEnergy -= 3;
    bestExpand.hex.owner = 'a'; bestExpand.hex.str = 1;
  }
}
✏️ Feladat

Adj hozzá egy "támadó stratégiát" az AI-hoz:

  • Ha az AI >60%-ban kontrollálja a térképet, preferálja a támadást a terjeszkedés helyett
  • Implementáld: const aiControl = aiHexes().length / G.hexes.length
  • Ha aiControl > 0.6, az attack score-t szorozd meg 2-vel
  • Teszteld nehéz szinten: az AI agresszívabb lesz ha vezet?

🧠 Miért kap az AI nagyobb energiabevételt nehéz szinten az aiEnergy: 1.5 szorzóval?

Mert az AI terjeszkedőbb szektor-típusokat választ
Ez a "resource cheating" — az AI nem játszik teljesen fair, hanem kap egy bónuszt ami a játékosnál nincs meg, kompenzálva hogy a szabályalapú AI nem tud ténylegesen "jobban gondolkodni"
Mert nehéz szinten több szektort irányít az AI kezdéskor
Csak kozmetikai különbség, a valódi nehézség a noise paraméter
AI Fejezet · 10. Lecke

📐 AI tervezési elvek — Mit tanultunk?

Három különböző játék, három különböző AI megközelítés. Hogyan általánosítsuk ezeket? Mit vigyünk magunkkal a következő játékba?

⏱ 45 perc
🎯 AI tervezési minták · playtesting · általánosítás

1A három AI összehasonlítása

JátékAI típusDöntés alapjaMi teszi érdekessé?
🐢 Teknős Verseny Célkövető + állapotgép Súlyozott scoring, időzített döntés aiErr (emberies hiba), állapotok, nehézség skálázás
🏰 Castle Siege Útvonalkövető (nincs saját döntés) Fix célirány + rugós Y-igazítás Típus-változatosság, hullám-rendszer, pályaskálázás
🚀 Galactic Conquest Stratégiai értékelő Kandidáns-lista + zaj-torzított rendezés 3 akciótype, noise szint, resource advantage

2Általánosítható AI minták

Általános AI sablon — bármilyen játékhozJavaScript
// ═══ ÁLTALÁNOS AI STRUKTÚRA ═══════════════════════════════

// 1. ÁLLAPOTGÉP: mód meghatározása
function updateState(ai, world) {
  if (ai.hp < 20)       ai.state = 'fleeing';
  else if (ai.hp > 80)  ai.state = 'aggressive';
  else                   ai.state = 'balanced';
}

// 2. KANDIDÁNS LISTA: lehetséges akciók összegyűjtése
function buildCandidates(ai, world) {
  return world.targets
    .filter(t => isVisible(ai, t))  // látótávolságon belül
    .map(t => ({
      target: t,
      score: scoreTarget(ai, t)     // multi-faktor pontszám
    }));
}

// 3. DÖNTÉS: legjobb jelölt + zaj
function decide(candidates, difficulty) {
  const noise = [.8, .35, .05][difficulty]; // könnyű → nehéz
  candidates.sort((a, b) =>
    (b.score + Math.random()*noise) - (a.score + Math.random()*noise)
  );
  return candidates[0]?.target ?? null;
}

// 4. VÉGREHAJTÁS: időzített, nem minden frame-ben
function updateAI(ai, world) {
  updateState(ai, world);           // állapot mindig frissül
  ai.timer--;
  if (ai.timer > 0) return;         // döntési késés
  ai.timer = 20 + ~~(Math.random()*30);

  const candidates = buildCandidates(ai, world);
  ai.target = decide(candidates, DIFF); // → cél meghatározva
  // Az actuális mozgás a target felé minden frame-ben fut
}

3Az 5 aranyszabály

🎯
1. Mindig legyőzhető legyen

Az AI nem nyerhet tökéletesen. A aiErr, a noise és a jumpChance mind erre szolgál — szándékos tökéletlenség.

2. Ne döntsön minden frame-ben

Az aiTimer késleltetés emberi reakcióidőt szimulál. Gyors döntés robotossá teszi az AI-t — a véletlenszerű időköz természetesebben hat.

📊
3. Konfig objektumban tartsd a paramétereket

A DIFF_CFG és AI_DIFF tömbök mintájára: minden AI paramétert egy helyen, a kódban ne legyenek szétszórt magic number-ek.

🧪
4. Playtesting a valódi debugger

A legjobb AI paramétereket nem számolni kell — ki kell próbálni. Egy ülés alatt derül ki hogy az AI unalmas-e vagy frusztráló.

🧩
5. Egyszerű szabályok, komplex viselkedés

A Galactic AI 30 sora háromféle stratégiát produkál. A Teknős AI 20 sora emberi hibákat szimulál. A legmeggyőzőbb AI rendszerek egyszerű elvekből épülnek fel.

✏️ Összefoglaló feladat

Tervezz egy saját AI-t egy egyszerű Pong klónhoz:

  • Az AI ütő kövesse a labda Y pozícióját — de lassan (spring dampening: diff * 0.05)
  • Adj hozzá aiErr-t: az ütő szisztematikusan kicsit feljebb vagy lejjebb céloz
  • Implementálj 3 nehézségi szintet: a könnyű ütő lassabb (diff*0.02), a nehéz gyorsabb (diff*0.12)
  • Adj hozzá állapotot: ha a labda közeledik, "aggressive" mód (gyorsabb); ha távolodik, "idle" (lassú visszatérés középre)

🧠 Melyik a leghatékonyabb módszer az AI nehézségi szint növelésére?

Teljesen más AI logikát írni minden szinthez
Az AI reakcióidejét nullára csökkenteni
Paraméterek kombinálása: reakcióidő, hibamérték, látótávolság, resource bónusz egyszerre — a DIFF_CFG objektumban tartva, így egy helyen módosítható
Az AI-t minden körben optimális lépést választani hagyni (minmax algoritmus)