🤖 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ó.
1Az AI spektrum
| Típus | Hogyan dönt? | Előny | Hátrány | Pé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 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
- ▸ 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)
- ✗ 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ő
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?
🐢 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.
1Az alap AI vs. a súlyozott AI
Nézzük mi a különbség a jelenlegi egyszerű AI és a súlyozott döntéshozó között:
// ❌ 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: 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 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
// 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; }
Épits egy egyszerű scoring rendszert:
- Nyisd meg a
turtle_race.html-t, keresd meg azupdateAI()függvényt - Egészítsd ki az
scpontszá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)?
🐢 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.
1Az AI állapotai — FSM tervezés
Feltétel: energia > 40%
Feltétel: energia < 40%
Feltétel: életei < 2
// 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; }
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.
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?
🎮 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ó.
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:
// 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; } } }); }
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?
🏰 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.
1A Castle Siege ellenség mozgáslogikája
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.
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 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
// 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ípus | Stratégia | Ellene hatékony | Ellene kevésbé hatékony |
|---|---|---|---|
| 🟢 Goblin | Sebesség — átrohan mielőtt a torony sokat lő | Gyors tüzelésű torony, hős | Erős de lassú torony |
| 🟤 Ork | Kiegyensúlyozott — semmiben nem extremum | Minden normál torony | — |
| 🟫 Troll | HP-tank — felszívja a tornyok lövedékeit | Nagy sebzésű torony, kőzár (lassít) | Gyors, kis sebzésű torony |
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á: hae.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?
🌊 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.
1A genLevel() — 50 pálya egyetlen függvényből
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; }
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
// Ú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 } } }
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.
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)?
🚀 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.
1A hex-grid koordináta rendszer
// 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 }
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
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 ... } }
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.
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?
🚀 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.
1Rendezés és legjobb lépés kiválasztása
// ─── 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; }
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?
// 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
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?
🎯 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?
1A DIFF_CFG részletesen
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
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
// 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; } }
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?
📐 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?
1A három AI összehasonlítása
| Játék | AI típus | Döntés alapja | Mi 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 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
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.
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.
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.
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ó.
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.
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)