GitHub avatar

Fox's Blog

My dumb AI for Nausicaa

A heuristic coefficient-based AI with hyperparameters that randomly

My dumb AI for Nausicaa

You know those projects that start with "hey what if I made a chess game with mythologies?" and end with a thing where the AI decides its own hyper-parameters every 5 turns?

Nausicaa is exactly that. A turn-based board game where you build your deck of mythological creatures, manage your mana, deploy units on a 10x8 grid. And there's an AI that has identity crises.

I spent a fair bit of time on this AI, and the result is pretty unmanageable xD

The game itself

Before talking about the brain, you gotta understand the body:

  • 10x8 board, 2-row deployment zone per player
  • Mana starts at 1, +1 per turn, max 6. You spend it to summon, attack, use abilities
  • Goal: kill the enemy Oracle

12 units, different costs and movement patterns:

Unit Cost Movement HP
Oracle 0 King (8 directions) 1
Goblin 1 Forward 3 squares 1
Harpy 1 King (8 directions) 1
Naiad 1 Diagonal 1
Griffin 2 Jump 2 squares 2
Siren 2 Lateral 1
Centaur 2 Knight (L-shape) 2
Archer 3 Lateral 1
Phoenix 3 Diagonal (dark squares) 1
Shapeshifter 4 Swap places 1
Seer 4 None (generates mana) 1
Titan 6 Limited (area attack) 3

Each unit has its own attack pattern. The Siren hits in 4 diagonals, the Archer shoots from 3 squares away, the Titan destroys everything around it on summon. Basically chess with mythos and deckbuilding xD

How I made the CPU think

The basic idea is stupidly simple: every enemy unit has an attractiveness coefficient. The more dangerous it is, the more the AI wants to deal with it.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle at 100 -- makes sense, it's the win condition. Titan at 95 because it OSes everything next to it on summon. Goblin at 20, it's a grunt, nobody cares.

Then for each pair of units (one ally, one enemy), I calculate:

interest = attractiveness × coeff_attract / (distance × coeff_dist)

Basically: the more dangerous and closer you are, the more the AI wants to wreck you.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

The coefficient shuffle

Here's where it gets fun -- the importance coefficients randomly change every 5 turns.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

One turn the AI goes hyper aggro (attract at 95, distance at 5), crossing everything to kill your Oracle. Next turn it prioritizes distance and repositions.

This is stolen from Pac-Man ghosts -- Blinky chases, Pinky ambushes. Here the AI changes "personality" every phase.

Result: you can't predict the AI over a full game. The CPU never plays the same match twice.

The Oracle is a coward

The enemy Oracle runs away. Literally.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

It calculates the direction opposite to the threat and bolts. If there's a wall, it finds the nearest free square in that direction.

You spend 3 turns getting close to the Oracle, and bam it's run off like a scaredy cat xD

The decision loop

Here's how the AI decides:

  1. If I lost my Oracle (dead), place a new one
  2. Calculate the coefficient for every ally → enemy pair
  3. Pick the best pair
  4. If the unit can attack the target from its position → attack
  5. If I have less than 4 units → summon the cheapest available from hand
  6. Otherwise, move toward the target (movement square closest to the enemy)
  7. If enough mana (> 2), dash (double move) to get even closer
  8. If the unit is the Oracle → flee
mermaid diagram
async makeAction(dash=false) {
    // all of this in sequence
    // the CPU dashes if it has enough mana
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Why Euclidean distance

I use Euclidean distance:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Why not Manhattan? Because units have varied movement patterns (L-shape like the knight, diagonal, etc). Bird's-eye distance is a better approximation of danger.

Why not minimax

I could've coded a classic minimax. But with 12 unit types, different movement patterns, special abilities... the game tree explodes so fast it becomes unplayable. The heuristic approach makes smart choices without exploring 10 million states.

What's cool

The attractiveness system creates funny dilemmas:

  • The Seer (70) generates mana. If you leave it alive, the opponent has more resources. But the Titan (95) is even more dangerous.
  • The Shapeshifter (90) can swap places with any unit. It can steal your Oracle.
  • The Harpy (50) has an explosive attack that also kills it. Not a priority... until it's next to 3 of your units.

The AI evaluates overall danger based on positions, not just raw stats.

There's also a activateSimulation() function to test scenarios without replaying a full game:

activateSimulation() {
    // Place specific units on the board
    // Useful for debugging the AI
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

What's missing

If I had more time:

  • The AI reacts to the current state, it doesn't predict what the player will do
  • It doesn't plan its hand over multiple turns
  • The Shapeshifter and Centaur have abilities it under-uses
  • Reinforcement learning: make it play against itself to tune the coefficients

But for a browser game it does the job. Some friends manage to lose against it, so it's good enough xD

Try it

Live at nausicaa-game.github.io. Click "JOUER", CPU mode ON, and watch the AI do its thing.

Tip: let the AI play against itself. You'll see aggressive phases, then poof it backs off completely.

The code is on GitHub in js/cpu.js.

3 takeaways:

  1. Heuristic coefficients -- no minimax, each unit has an attractiveness score
  2. Coefficients that change every 5 turns -- the AI alternates aggro and control, Pac-Man style
  3. The Oracle runs away -- it calculates the direction opposite to the threat and books it

If you have ideas to make the AI even more vicious, open an issue. I have plans for a version that learns from its losses, but that'll be for another article xD

Mon IA à la con pour Nausicaa

Une IA à coefficients heuristiques, des hyper-paramètres qui

Mon IA à la con pour Nausicaa

Y'a des projets qui commencent par "tiens si je faisais un jeu d'échecs avec des mythologies ?" et qui finissent par un truc avec une IA qui décide de ses propres hyper-paramètres tous les 5 tours.

Nausicaa c'est ça. Un jeu de plateau au tour par tour où tu construis ton deck de créatures mythologiques, tu gères ton mana, tu déploies des unités sur un plateau 10x8. Et y'a une IA qui a des crises de personnalité.

J'ai passé pas mal de temps sur cette IA, et le résultat est assez ingérable xD

Le jeu en vrai

Avant de parler du cerveau, faut comprendre le corps :

  • Plateau 10x8, zone de déploiement de 2 rangées par joueur
  • Mana commence à 1, +1 par tour, max 6. Tu dépenses pour invoquer, attaquer, utiliser des capacités
  • But : buter l'Oracle adverse

12 unités, des coûts et des patterns de mouvement différents :

Unit Coût Mouvement PV
Oracle 0 Roi (8 directions) 1
Gobelin 1 Avant 3 cases 1
Harpie 1 Roi (8 directions) 1
Naïade 1 Diagonale 1
Griffin 2 Hop 2 cases 2
Sirène 2 Latéral 1
Centaure 2 Cavalier (en L) 2
Archer 3 Latéral 1
Phénix 3 Diagonale (cases sombres) 1
Métamorphe 4 Échange de place 1
Voyant 4 Aucun (genère du mana) 1
Titan 6 Limité (attaque zone) 3

Chaque unité a son propre pattern d'attaque. La Sirène tape dans les 4 diagonales, l'Archer à distance sur 3 cases, le Titan détruit tout autour à l'invocation. Bref un jeu d'échecs avec du mytoches et du deckbuilding xD

Comment j'ai fait réfléchir le CPU

L'idée de base est débilement simple : chaque unité ennemie a un coefficient d'attractivité. Plus elle est dangereuse, plus l'IA veut s'en occuper.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle à 100 -- logique, c'est la win condition. Titan à 95 parce qu'il OS tout à côté à l'invocation. Gobelin à 20, c'est un fantassin, on s'en branle.

Ensuite pour chaque paire d'unités (une alliée, une ennemie), je calcule :

interet = attractivite × coeff_attract / (distance × coeff_dist)

En gros : plus t'es dangereux et proche, plus l'IA veut te défoncer.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

Le coup des coefficients qui changent

Là où c'est marrant c'est que les coefficients d'importance changent aléatoirement tous les 5 tours.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Un coup l'IA va hyper agressive (attract à 95, distance à 5), elle traverse tout pour buter ton Oracle. Le coup d'après elle priorise la distance et se repositionne.

C'est piqué aux fantômes de Pac-Man -- Blinky chasse, Pinky embusque. Ici l'IA change de "personnalité" toutes les phases.

Résultat : impossible de prédire l'IA sur une partie entière. Le CPU fait jamais deux fois le même match.

L'Oracle est une lopette

L'Oracle ennemi fuit. Littéralement.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Il calcule la direction opposée à la menace et se barre. Si y'a un mur, il cherche la case libre la plus proche dans cette direction.

Tu passes 3 tours à t'approcher de l'Oracle, et paf il s'est cassé comme une fillette xD

La boucle de décision

Voilà comment l'IA décide :

  1. Si j'ai plus d'Oracle (mort), en placer un nouveau
  2. Calculer le coefficient pour chaque couple unité alliée → unité ennemie
  3. Choisir la meilleure paire
  4. Si l'unité peut attaquer la cible depuis sa position → attaque
  5. Si j'ai moins de 4 unités → invoquer la moins chère disponible depuis la main
  6. Sinon, se déplacer vers la cible (case de mouvement la plus proche de l'ennemi)
  7. Si assez de mana (> 2), dash (double mouvement) pour se rapprocher encore
  8. Si l'unité est l'Oracle → fuir
mermaid diagram
async makeAction(dash=false) {
    // tout ça en séquence
    // le CPU dash si il a assez de mana
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Pourquoi la distance euclidienne

J'utilise la distance euclidienne :

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Pourquoi pas Manhattan ? Parce que les unités ont des patterns de mouvement variés (L comme le cavalier, diagonale, etc). La distance à vol d'oiseau est une meilleure approximation du danger.

Pourquoi pas du minimax

J'aurais pu coder un minimax classique. Mais avec 12 types d'unités, des patterns de mouvement différents, des capacités spéciales... l'arbre de jeu explose tellement vite que ça devient injouable. L'approche heuristique fait des choix intelligents sans explorer 10 millions d'états.

Ce qui est cool

Le système d'attractivité crée des dilemmes rigolos :

  • Le Voyant (70) génère du mana. Si tu le laisses vivre, l'adversaire a plus de ressources. Mais le Titan (95) est encore plus dangereux.
  • Le Métamorphe (90) peut échanger sa place avec n'importe quelle unité. Il peut voler ton Oracle.
  • L'Harpie (50) a une attaque explosive qui la tue aussi. Pas prioritaire... jusqu'à ce qu'elle soit à côté de 3 de tes unités.

L'IA évalue le danger global selon les positions, pas juste les stats brutes.

Y'a aussi une fonction activateSimulation() pour tester des scénarios sans refaire une partie :

activateSimulation() {
    // Place des unités spécifiques sur le plateau
    // Utile pour debugger l'IA
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Ce qui manque

Si j'avais plus de temps :

  • L'IA réagit à l'état actuel, elle prédit pas ce que le joueur va faire
  • Elle planifie pas sa main sur plusieurs tours
  • Le Métamorphe et le Centaure ont des capacités qu'elle sous-exploit
  • Apprentissage par renforcement : la faire jouer contre elle-même pour ajuster les coeffs

Mais pour un jeu de navigateur ça fait le taf. Des potes arrivent à perdre contre, donc c'est bon xD

Teste

Dispo sur nausicaa-game.github.io. Tu cliques sur "JOUER", CPU mode ON, et tu regardes l'IA faire.

Conseil : laisse l'IA jouer contre elle-même. Tu vas voir des phases agressives, puis pfft elle recule tout.

Le code est sur GitHub dans js/cpu.js.

3 trucs :

  1. Coefficients heuristiques -- pas de minimax, chaque unité a une attractivité
  2. Coeffs qui changent tous les 5 tours -- l'IA alterne agressivité et contrôle, façon Pac-Man
  3. L'Oracle fuit -- il calcule la direction opposée à la menace et se casse

Si t'as des idées pour rendre l'IA encore plus vicieuse, ouvre une issue. J'ai des plans pour une version qui apprend de ses défaites, mais ça sera pour un prochain article xD

我给 Nausicaa 写的那个沙雕 AI

一个基于启发式系数的 AI,超参数每 5 回合随机变化,还有会逃跑的神谕 -- 深入一款神话策略棋盘游戏的大脑。

我给 Nausicaa 写的那个沙雕 AI

有些项目从"要不做个神话主题的象棋?"开始,最后搞出一个每 5 回合自己改超参数的人工智障。

Nausicaa 就是这样。一个回合制棋盘游戏,你组神话生物卡组、管蓝条、在 10x8 的板子上拍单位。还有个 AI 会人格分裂。

我在这 AI 上花了不少时间,结果完全管不住它 xD

游戏本体

聊脑子之前,先看看身体:

  • 10x8 棋盘,每人 2 行部署区
  • 蓝条从 1 开始,每回合 +1,上限 6。用来召唤、攻击、放技能
  • 目标:干爆对面的 Oracle

12 个单位,费用和移动方式各不相同:

单位 费用 移动 血量
Oracle 0 王 (8 方向) 1
哥布林 1 前进 3 格 1
鹰身女妖 1 王 (8 方向) 1
那伊阿得 1 对角线 1
狮鹫 2 跳 2 格 2
塞壬 2 横向 1
半人马 2 骑士 (L 形) 2
弓箭手 3 横向 1
凤凰 3 对角线 (深色格) 1
变形者 4 交换位置 1
先知 4 不动 (产蓝) 1
泰坦 6 有限 (范围攻击) 3

每个单位有各自的攻击模式。塞壬打 4 条对角线,弓箭手隔 3 格远程输出,泰坦上场就炸一片。总之就是个带神话生物和组牌要素的象棋 xD

我怎么让 CPU 思考的

基本思路蠢得一批:每个敌方单位有个吸引力系数。越危险,AI 就越想搞它。

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle 100 ---- 废话,赢了条件。泰坦 95,上场秒一片。哥布林 20,就是个炮灰,谁管他。

然后对每对单位(一个友方一个敌方),我算:

interet = attractivite × coeff_attract / (distance × coeff_dist)

简单说:你越危险越近,AI 就越想干你。

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

系数会变的骚操作

好玩的地方在于,这些重要性系数每 5 回合随机变一次。

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

上一秒 AI 还猛得一批(吸引 95,距离 5),直接穿地图干你 Oracle。下一秒它又优先考虑距离开始 reposition。

这招是从 Pac-Man 的幽灵学的 ---- Blinky 追人,Pinky 埋伏。这里 AI 每个阶段换一次"人格"。

结果:整局游戏你根本猜不透 AI。 CPU 永远不会打出两局一样的操作。

Oracle 是个怂包

敌方 Oracle 会跑。字面意思。

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

它算出威胁的反方向然后溜了。有墙的话就找那个方向上最近的空格。

你花了 3 回合摸过去,啪一下它就跑了,跟个小姑娘似的 xD

决策循环

AI 的决策流程:

  1. 如果我没 Oracle 了(死了),放个新的
  2. 算每对友方→敌方单位的系数
  3. 选最优配对
  4. 如果单位当前位置能打到目标 → 攻击
  5. 如果我少于 4 个单位 → 从手牌召唤最便宜的
  6. 否则,往目标移动(离敌人最近的移动格)
  7. 如果蓝条够(> 2),冲刺(二段移动)再拉近距离
  8. 如果单位是 Oracle → 跑路
mermaid diagram
async makeAction(dash=false) {
    // 全部按顺序来
    // CPU 蓝条够就冲刺
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

为什么用欧几里得距离

我用的欧几里得距离:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

为啥不用 Manhattan?因为单位有各种移动模式(骑士 L 形、对角线等等)。直线距离更能体现实际威胁。

为啥不用 minimax

我也可以写个经典 minimax。但 12 种单位、不同移动模式、特殊技能……游戏树膨胀得飞起,根本玩不动。启发式方法不用搜一千万个状态就能做出聪明选择。

酷的地方

吸引力系统整出了些好玩的 dilemma:

  • 先知 (70) 产蓝。放着不管对面就有更多资源。但泰坦 (95) 更危险。
  • 变形者 (90) 能跟任何单位换位。他能直接偷你 Oracle。
  • 鹰身女妖 (50) 有自爆攻击。不是优先目标……直到它站到你 3 个单位旁边。

AI 是根据位置评估全局危险,不只是看面板数据。

还有个 activateSimulation() 函数用来测试场景,不用重开一局:

activateSimulation() {
    // 在棋盘上放特定单位
    // 用来 debug AI
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

还缺什么

如果有更多时间:

  • AI 只反应当前状态,不会预测玩家下一步
  • 不会规划手牌打 combo
  • 变形者和半人马的能力它用得不够好
  • 强化学习:让它跟自己打来调系数

但一个网页游戏够用了。我朋友都能输给它,所以还行 xD

试试

在 nausicaa-game.github.io 上就能玩。点"JOUER",开 CPU 模式,看 AI 表演。

建议:让 AI 自己打自己。你会看到它一波猛攻,然后突然全缩回去了。

代码在 GitHub 的 js/cpu.js。

3 个要点:

  1. 启发式系数 ---- 不用 minimax,每个单位有吸引力值
  2. 每 5 回合换系数 ---- AI 在激进和控场之间切换,学 Pac-Man
  3. Oracle 会跑 ---- 算威胁反方向然后溜

你有啥让 AI 更阴险的点子就去开 issue。我计划搞个能学习失败的版本,不过那下篇文章再说了 xD

Nausicaa用のクソAI

ヒューリスティック係数ベースのAI、5ターンごとにランダムに変わるハイパーパラメータ、逃げるオラクル -- 神話ストラテジーボードゲームの脳内に潜入。

俺のクソAI for Nausicaa

「神话テーマのチェス作ってみるか〜」って軽い気持ちで始めたら、5ターンごとにハイパーパラメータを自分で変えるAIが出来上がった。

Nausicaaはそんなゲーム。ターン制のボードゲームで、神话のクリーチャーをデッキ构建、マナを管理しながら10x8の盤面にユニットを展開する。んで、AIが人格変わるっていう xD

このAIにかなり时间かけたけど、结果はめちゃくちゃ手に负えない感じになった xD

ゲームの基本

脳みその话をする前に、まずはゲーム自体を理解しないとな:

  • 10x8の盤面、プレイヤーごとに2列の配置ゾーン
  • マナは1からスタート、毎ターン+1、上限6。召唤・攻撃・アビリティに使う
  • 目的:相手のOracleを杀す

12体のユニット、それぞれコストと移动パターンが违う:

Unit コスト 移动 HP
Oracle 0 キング(8方向) 1
Gobelin 1 前に3マス 1
Harpie 1 キング(8方向) 1
Naïade 1 斜め 1
Griffin 2 2マスジャンプ 2
Sirène 2 横 1
Centaure 2 骑士(L字) 2
Archer 3 横 1
Phénix 3 斜め(暗いマスのみ) 1
Métamorphe 4 位置交换 1
Voyant 4 なし(マナ生成) 1
Titan 6 制限あり(範囲攻撃) 3

ユニットごとに攻撃パターンも违う。Sirèneは斜め4方向、Archerは3マス先まで远距离、Titanは召唤时周囲を壊灭させる。つまり神话xデッキ构建チェスって感じ xD

CPUに考えさせる方法

基本アイデアはシンプルにバカみたい:敌ユニットそれぞれに魅力度(アトラクティブネス)系数をつける。危険なヤツほどAIが狙う。

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracleが100なのは当然、胜ち条件だからな。Titanが95なのは召唤时に周囲をOSするから。Gobelinは20、ただの步兵だからどうでもいい。

で、味方と敌のユニットのペアごとにこれを计算する:

interet = attractivite × coeff_attract / (distance × coeff_dist)

つまり:危険で近いほど、AIがぶっ飞ばしたがる。

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

系数がコロコロ変わるやつ

ここが面白いとこなんだけど、重要度系数が5ターンごとにランダムで変わる。

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

ある时は超アグレッシブ(魅力95、距离5)で、Oracleをぶっ杀しに一直线。次のターンは距离优先で再配置する。

これ、パックマンの幽霊からパクってるんだよね。Blinkyは追跡、Pinkyは待ち伏せ。ここではAIがフェーズごとに「性格」を変える。

结果:1试合全体を通してAIの行动を予测するのは不可能。 CPUが同じ试合を2度とやらない。

Oracleは弱虫

敌のOracleは逃げる。文字通り。

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

威胁の逆方向を计算して逃げまくる。壁があったらその方向で一番近い空きマスを探す。

3ターンかけてOracleに近づいて、そしたらもうチビっ子みたいに逃げてるんだぜ xD

决定ループ

AIの判断フローはこんな感じ:

  1. Oracleがもういなかったら(死んだら)、新しいのを配置
  2. 味方ユニット→敌ユニットのペアごとに系数を计算
  3. ベストなペアを选択
  4. 今の位置からターゲットを攻撃できるなら → 攻撃
  5. ユニットが4体未満なら → 手札から一番安いのを召唤
  6. それ以外なら → ターゲットに移动(敌に一番近い移动マスへ)
  7. マナが余ってたら(> 2)、ダッシュ(2回移动)でさらに接近
  8. ユニットがOracleなら → 逃げる
mermaid diagram
async makeAction(dash=false) {
    // 全部顺番に处理
    // マナが余ってたらCPUはダッシュする
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

なんでユークリッド距离なの

ユークリッド距离を使ってる:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

マンハッタン距离じゃない理由?ユニットの移动パターンがバラバラだから(L字とか骑士移动、斜め移动とか)。鸟瞰距离の方が危険度の近似としてマシなんだよね。

なんでミニマックスじゃないの

普通にミニマックス组むこともできた。でも12种类のユニット、违う移动パターン、特殊アビリティ…ゲーム木が爆発的に増えてやってられなくなる。ヒューリスティックなアプローチなら、1000万の状态を探索しなくてもスマートな判断ができる。

イケてるとこ

魅力度システムが面白いジレンマを生む:

  • Voyant(70)はマナを生成する。生かしておくと相手のリソースが増える。でもTitan(95)の方がまだ危険。
  • Métamorphe(90)はどのユニットとも位置を交换できる。Oracleを盗むことも可能。
  • Harpie(50)は自分も死ぬ爆発攻撃を持つ。优先度低い…でも自分のユニット3体の隣にいたら话は别。

AIは単なる生のステータスじゃなくて、位置に応じた全体的な危険度を评価してる。

あとactivateSimulation()って関数があって、フルで试合しなくてもシナリオテストできる:

activateSimulation() {
    // 特定のユニットを盘面に配置
    // AIデバッグに便利
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

足りないとこ

もっと时间があったらやりたいこと:

  • AIは今の状态にしか反应してない。プレイヤーの行动予测はしない
  • 手札を复数ターンにわたって计画してない
  • MétamorpheとCentaureの能力を活かしきれてない
  • 强化学习:自分自身と対戦させて系数を调整する

でもブラウザゲームとしては十分。知り合いがこれに负けてるから、まあ及第点だな xD

试してみて

nausicaa-game.github.io で公开中。「JOUER」をクリックしてCPUモードON、AIの动きを见てみて。

アドバイス:AI同士で戦わせてみるといい。超アグレッシブなフェーズのあと、いきなり全员下がったりするから。

コードはGitHubのjs/cpu.jsにある。

3つのポイント:

  1. ヒューリスティック系数 -- ミニマックスなし、ユニットごとに魅力度がある
  2. 5ターンごとに系数が変わる -- AIが攻势と管理を交互に、パックマン方式
  3. Oracleは逃げる -- 威胁の逆方向を计算して速攻离脱

AIをもっと凶悪にするアイデアがあったらIssueを开いてくれ。败北から学ぶバージョンの计画もあるけど、それはまた次の記事でな xD

내가 Nausicaa용으로 만든 좆같은 AI

휴리스틱 계수 기반 AI, 5턴마다 랜덤으로 바뀌는 하이퍼파라미터, 도망치는 오라클 -- 신화 전략 보드 게임의 두뇌 속으로.

내 막장 AI for Nausicaa

"에에... 신화 테마로 체스 게임 한 번 만들어볼까?" 에서 시작한 프로젝트가 5턴마다 자기 파라미터를 바꾸는 AI를 달고 끝났다 xD

Nausicaa는 그런 게임. 턴제 보드게임 + 신화 생물 덱 빌딩 + 마나 관리, 10x8 보드 위에서 유닛을 배치하는 방식이야.

그리고 AI가 정체성 위기를 겪음 ㅋㅋ

AI 만드는데 꽤 공 들였는데, 결과물은 꽤 답이 없다 xD

게임은 뭐하는 거냐면

뇌 얘기하기 전에 몸통부터 설명해야지:

  • 10x8 보드, 각 플레이어당 2줄 배치 구역
  • 마나는 1에서 시작, 턴마다 +1, 최대 6. 소환/공격/스킬에 사용
  • 목표: 상대 Oracle을 개발살내기

12종 유닛, 각자 코스트와 이동 패턴이 다름:

Unit 코스트 이동 HP
Oracle 0 킹 (8방향) 1
Gobelin 1 앞으로 3칸 1
Harpie 1 킹 (8방향) 1
Naïade 1 대각선 1
Griffin 2 2칸 점프 2
Sirène 2 좌우 1
Centaure 2 나이트 (L자) 2
Archer 3 좌우 1
Phénix 3 대각선 (어두운 칸만) 1
Métamorphe 4 자리 바꾸기 1
Voyant 4 없음 (마나 생성) 1
Titan 6 제한됨 (범위 공격) 3

각 유닛은 자기만의 공격 패턴이 있음. Sirène은 대각선 4방향, Archer는 3칸 원거리, Titan은 소환되자마자 주변 다 쓸어버림. ㅇㅇ 신화+덱빌딩 체스라고 보면 됨 xD

CPU를 어떻게 생각하게 만들었나

기본 아이디어는 존나 단순함: 적 유닛마다 어그로 계수가 있음. 위험할수록 AI가 더 집중함.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle 100 -- 당연하지, 이걸 따야 승리임. Titan 95 -- 소환되면 옆에 있는 놈들 다 원킬 내니까. Gobelin 20은 그냥 잡몹, 신경 쓸 가치 없음.

그 다음 모든 유닛 쌍 (아군 1, 적군 1) 마다 계산:

interest = attractiveness × coeff_attract / (distance × coeff_dist)

쉽게 말하면: 위험할수록 + 가까울수록 AI가 더 패고 싶어함.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

계수가 바뀌는 미친 짓

웃긴 점은 중요도 계수가 5턴마다 랜덤으로 바뀜.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

한 번은 AI가 개돌 모드 (어그로 95, 거리 5) 로 닥돌해서 Oracle 따버리고, 다음 번엔 거리 위주로 다시 포지셔닝함.

팩맨 유령들한테 아이디어를 땀 -- Blinky는 추적, Pinky는 매복. 여기서 AI도 페이즈마다 "성격"이 바뀌는 셈.

결과: 게임 내내 AI를 예측하는 게 불가능함. 똑같은 경기를 두 번 하는 법이 없음.

Oracle은 겁쟁이임

적 Oracle은 도망감. 말 그대로.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

위협의 반대 방향을 계산해서 튐. 벽이 있으면 그 방향에서 가장 가까운 빈 칸을 찾음.

3턴 동안 Oracle에 접근했는데, ㅅㅂ 도망가버림 ㅋㅋ xD

결정 루프

AI가 결정하는 방식:

  1. Oracle이 죽었으면 새로 배치
  2. 모든 아군→적군 유닛 쌍에 대해 계수 계산
  3. 최적의 쌍 선택
  4. 지금 위치에서 공격 가능하면 → 공격
  5. 유닛 4개 미만이면 → 손에서 가장 싼 유닛 소환
  6. 아니면 적에게 이동 (적과 가장 가까운 이동 가능 칸으로)
  7. 마나가 충분하면 (> 2) → 대시 (2연속 이동) 로 더 접근
  8. 유닛이 Oracle이면 → 도망
mermaid diagram
async makeAction(dash=false) {
    // 전부 순차적으로
    // 마나 충분하면 CPU가 대시함
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

왜 유클리드 거리냐면

유클리드 거리를 씀:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

맨해튼 거리는 왜 안 씀? 유닛들 이동 패턴이 다양해서 (L자, 대각선 등등). 직선 거리가 위험도를 더 잘 나타냄.

미니맥스는 왜 안 함

미니맥스로 짤 수도 있었음. 근데 유닛 12종, 이동 패턴 다 다르고, 특수 능력까지... 게임 트리가 미친 듯이 불어나서 플레이가 불가능해짐. 휴리스틱 접근법이 1천만 상태를 탐색하지 않고도 똑똑한 선택을 할 수 있게 해줌.

쩌는 점

어그로 시스템이 꽤 재밌는 딜레마를 만듦:

  • Voyant (70)는 마나를 생성함. 냅두면 적이 자원 더 먹음. 근데 Titan (95)이 더 위험함.
  • Métamorphe (90)는 아무 유닛이랑 자리 바꾸기 가능. Oracle을 스틸할 수 있음.
  • Harpie (50)는 자폭 공격이라 자기 자신도 뒤짐. 우선순위 낮음... 근데 내 유닛 3개 옆에 붙으면 얘기가 다름.

AI는 순수 능력치뿐만 아니라 포지션 기반으로 전체 위험도를 평가함.

시나리오 테스트용 activateSimulation() 함수도 있음:

activateSimulation() {
    // 보드에 특정 유닛 배치
    // AI 디버깅용
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

아쉬운 점

시간 더 있었으면:

  • AI는 현재 상태에만 반응함, 플레이어의 다음 행동은 예측 못 함
  • 여러 턴에 걸친 핸드 플랜이 없음
  • Métamorphe랑 Centaure 능력을 덜 활용함
  • 강화학습: 자기랑 붙여서 계수 튜닝

근데 브라우저 게임치고는 충분함. 주변 친구들도 지기도 함 ㅋㅋ xD

플레이해보셈

nausicaa-game.github.io 에서 가능. "JOUER" 누르고 CPU 모드 켜면 AI가 뭐 하는지 볼 수 있음.

팁: AI끼리 붙여봐. 공격적으로 가다가 갑자기 쭉 빠지는 꼴을 볼 수 있음.

코드는 GitHub js/cpu.js 에 있음.

3줄 요약:

  1. 휴리스틱 계수 -- 미니맥스 없음, 유닛마다 어그로 수치
  2. 5턴마다 계수 변경 -- AI가 팩맨처럼 공격/컨트롤 전환
  3. Oracle은 튐 -- 위협 반대 방향 계산해서 도주

AI를 더 빡치게 만드는 아이디어 있으면 이슈 남겨줘. 패배에서 배우는 버전도 구상 중이긴 한데, 그건 다음 글에서 ㅋㅋ xD

Nausicaa için salaş yapay zekâm

Sezgisel katsayı tabanlı bir yapay zeka, her 5 turda rastgele

Nausicaa için Salak Yapay Zekam

"Bi' satranç oyunu yapsam mitolojilerle falan?" diye başlayıp her 5 turda kendi hiper-parametrelerine karar veren bi' yapay zekayla biten projeler vardır ya.

İşte Nausicaa o. Sıra tabanlı bi' masa oyunu, mitolojik yaratıklardan desteni kuruyorsun, mana'yı yönetiyorsun, 10x8'lik bi' tahtada birimlerini konuşlandırıyorsun. Ve bi' tane de kişilik bozukluğu olan yapay zeka var xD

Bu yapay zekaya epey zaman harcadım ve sonuç tam bi' felaket xD

Oyun aslında ne

Beyni anlatmadan önce gövdeyi anlamak lazım:

  • 10x8 tahta, oyuncu başına 2 sıra konuşlandırma alanı
  • Mana 1'den başlar, her tur +1, max 6. Çağırmak, saldırmak, yetenek kullanmak için harcarsın
  • Amaç : rakibin Oracle'ını öldürmek

12 birim, farklı maliyetler ve hareket pattern'leri:

Unit Maliyet Hareket HP
Oracle 0 Kral (8 yön) 1
Goblin 1 3 kare ileri 1
Harpy 1 Kral (8 yön) 1
Naiad 1 Çapraz 1
Griffin 2 2 kare zıpla 2
Siren 2 Yanal 1
Centaur 2 At (L şeklinde) 2
Archer 3 Yanal 1
Phoenix 3 Çapraz (koyu kareler) 1
Metamorfoz 4 Yer değiştirme 1
Seer 4 Yok (mana üretir) 1
Titan 6 Sınırlı (alan saldırısı) 3

Her birimin kendine özgü saldırı pattern'i var. Siren 4 çapraza vuruyor, Archer 3 kare mesafeden vuruyor, Titan çağrılınca etrafındaki her şeyi mahvediyor. Kısacası mitoloji ve deste kurmacalı bi' satranç xD

CPU'yu nasıl düşündürdüm

Temel fikir salakça basit: her düşman biriminin bi' çekicilik katsayısı var. Ne kadar tehlikeliyse, yapay zeka onunla o kadar ilgilenmek istiyor.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle 100 -- mantıklı, win condition bu. Titan 95 çünkü çağrılınca yanındaki herkesi tek atıyor. Goblin 20, işte piyade, kim takar.

Sonra her birim çifti için (bir dost, bir düşman) şunu hesaplıyorum:

interet = attractivite × coeff_attract / (distance × coeff_dist)

Yani: ne kadar tehlikeli ve yakınsan, yapay zeka seni o kadar pataklamak istiyor.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

Katsayıların değiştiği an

İşin komik tarafı, önem katsayıları her 5 turda rastgele değişiyor.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Bi' anda yapay zeka aşırı agresif oluyor (çekicilik 95, mesafe 5), her şeyi geçip Oracle'ını öldürmeye geliyor. Sonraki elde mesafeyi önceliyor ve yeniden konuşlanıyor.

Bu Pac-Man hayaletlerinden çalıntı -- Blinky kovalar, Pinky pusu kurar. Burada yapay zeka her fazda "kişilik" değiştiriyor.

Sonuç: bi' maç boyunca yapay zekayı tahmin etmek imkansız. CPU asla aynı maçı iki kere oynamıyor.

Oracle ezik bi' şey

Düşman Oracle'ı kaçıyor. Gerçekten.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Tehdidin ters yönünü hesaplayıp kaçıyor. Duvar varsa o yöndeki en yakın boş kareyi arıyor.

3 tur boyunca Oracle'a yaklaşıyorsun, sonra puuf kaçıyor korkak gibi xD

Karar döngüsü

Yapay zeka şöyle karar veriyor:

  1. Oracle'ım kalmadıysa (öldüyse) yenisini koy
  2. Her dost → düşman birim çifti için katsayı hesapla
  3. En iyi çifti seç
  4. Birim bulunduğu yerden hedefe saldırabiliyorsa → saldır
  5. 4'ten az birimim varsa → eldeki en ucuz müsait birimi çağır
  6. Değilse, hedefe doğru hareket et (düşmana en yakın hareket karesi)
  7. Mana yeterliyse (> 2), dash yap (çift hareket) iyice yaklaşmak için
  8. Birim Oracle'sa → kaç
mermaid diagram
async makeAction(dash=false) {
    // hepsi sırayla
    // mana yetiyosa CPU dash yapar
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Neden öklid mesafesi

Öklid mesafesi kullanıyorum:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Neden Manhattan değil? Çünkü birimlerin hareket pattern'leri değişken (L şeklinde at gibi, çapraz, falan). Kuş uçuşu mesafe tehlikenin daha iyi bi' tahmini.

Neden minimax değil

Klasik bi' minimax yapabilirdim. Ama 12 birim tipi, farklı hareket pattern'leri, özel yeteneklerle... oyun ağacı o kadar hızlı patlıyor ki oynanamaz hale geliyor. Sezgisel yaklaşım 10 milyon durumu keşfetmeden akıllı seçimler yapıyor.

Havalı olan şeyler

Çekicilik sistemi komik ikilemler yaratıyor:

  • Seer (70) mana üretiyor. Onu yaşatırsan rakibin kaynağı artar. Ama Titan (95) daha tehlikeli.
  • Metamorfoz (90) herhangi bir birimle yer değiştirebiliyor. Oracle'ını çalabilir.
  • Harpy (50) patlayıcı bi' saldırıya sahip, kendini de öldürüyor. Öncelikli değil... ta ki 3 biriminin yanına gelene kadar.

Yapay zeka genel tehlikeyi pozisyonlara göre değerlendiriyor, sadece ham istatistiklere bakmıyor.

Ayrıca bi' activateSimulation() fonksiyonu var, maç yapmadan senaryo test etmek için:

activateSimulation() {
    // Tahtaya belirli birimleri yerleştirir
    // Yapay zeka debug'ı için kullanışlı
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Eksik olanlar

Daha fazla zamanım olsaydı:

  • Yapay zeka anlık duruma tepki veriyor, oyuncunun ne yapacağını tahmin etmiyor
  • Elini birkaç turluk planlamıyor
  • Metamorfoz ve Centaur'un yeteneklerini tam kullanamıyor
  • Pekiştirmeli öğrenme: kendi kendine oynayıp katsayıları ayarlaması

Ama browser oyunu için iş görüyor. Arkadaşlarım kaybedebiliyo, yani iyidir xD

Dene

nausicaa-game.github.io'da mevcut. "JOUER"a tıkla, CPU modu AÇIK, yapay zekanın yaptıklarını izle.

Tavsiye: yapay zekanın kendi kendine oynamasına izin ver. Agresif evreler göreceksin, sonra puf geri çekiliyor.

Kod GitHub'da, js/cpu.js içinde.

3 madde:

  1. Sezgisel katsayılar -- minimax yok, her birimin bi' çekiciliği var
  2. Her 5 turda değişen katsayılar -- yapay zeka agresiflik ve kontrol arasında gidip geliyor, Pac-Man vari
  3. Oracle kaçar -- tehdidin ters yönünü hesaplayıp sıvışır

Yapay zekayı daha da şerefsiz yapmak için fikirlerin varsa bi' issue aç. Kaybettiğinden ders alan bi' versiyon için planlarım var ama o başka bi' yazıya xD

La mia IA del cazzo per Nausicaa

Un'IA basata su coefficienti euristici, iperparametri che cambiano

La mia IA sballata per Nausicaa

Ci sono progetti che iniziano con "e se facessi un gioco di scacchi con mitologie?" e finiscono con un coso dotato di IA che si cambia gli iper-parametri da sola ogni 5 turni.

Nausicaa è questo. Un gioco da tavolo a turni dove costruisci il tuo mazzo di creature mitologiche, gestisci la mana, schieri unità su una plancia 10x8. E c'è un'IA con crisi di personalità.

Ci ho speso un sacco di tempo su sta IA, e il risultato è abbastanza ingovernabile xD

Il gioco per davvero

Prima di parlare del cervello, devi capire il corpo:

  • Plancia 10x8, zona di schieramento di 2 file per giocatore
  • Mana parte da 1, +1 per turno, max 6. La spendi per evocare, attaccare, usare abilità
  • Obiettivo: fottere l'Oracle avversario

12 unità, costi e pattern di movimento diversi:

Unit Costo Movimento PV
Oracle 0 Re (8 direzioni) 1
Goblin 1 Avanti 3 caselle 1
Arpia 1 Re (8 direzioni) 1
Naïade 1 Diagonale 1
Grifone 2 Salta 2 caselle 2
Sirena 2 Laterale 1
Centauro 2 Cavallo (a L) 2
Arciere 3 Laterale 1
Fenice 3 Diagonale (caselle scure) 1
Mutforma 4 Scambio di posto 1
Veggente 4 Nessuno (genera mana) 1
Titano 6 Limitato (attacco ad area) 3

Ogni unità ha il suo pattern d'attacco. La Sirena colpisce in 4 diagonali, l'Arciere a distanza su 3 caselle, il Titano distrugge tutto intorno all'evocazione. Insomma, scacchi con mitologia e deckbuilding xD

Come ho fatto a pensare al CPU

L'idea di base è stupidamente semplice: ogni unità nemica ha un coefficiente di attrattività. Più è pericolosa, più l'IA vuole occuparsene.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle a 100 -- logico, è la win condition. Titano a 95 perché OS tutto a lato all'evocazione. Goblin a 20, è un soldato semplice, chissene.

Poi per ogni coppia di unità (una alleata, una nemica), calcolo:

interesse = attrattività × coeff_attr / (distanza × coeff_dist)

In pratica: più sei pericoloso e vicino, più l'IA ti vuole spaccare.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

Il colpo dei coefficienti che cambiano

Il bello è che i coefficienti d'importanza cambiano a caso ogni 5 turni.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Un colpo l'IA è iper aggressiva (attr a 95, distanza a 5), attraversa tutto per fottere il tuo Oracle. Il colpo dopo prioritizza la distanza e si riposiziona.

Sta roba è rubata ai fantasmi di Pac-Man -- Blinky insegue, Pinky tende agguati. Qui l'IA cambia "personalità" ogni fase.

Risultato: impossibile prevedere l'IA in un'intera partita. Il CPU non fa mai due volte la stessa partita.

L'Oracle è un piagnona

L'Oracle nemico scappa. Letteralmente.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Calcola la direzione opposta alla minaccia e se ne va. Se c'è un muro, cerca la casella libera più vicina in quella direzione.

Passi 3 turni ad avvicinarti all'Oracle, e paf se n'è scappato come una donzella xD

Il loop decisionale

Ecco come decide l'IA:

  1. Se non ho più Oracle (morto), piazzarne uno nuovo
  2. Calcolare il coefficiente per ogni coppia unità alleata → unità nemica
  3. Scegliere la coppia migliore
  4. Se l'unità può attaccare il bersaglio dalla sua posizione → attacca
  5. Se ho meno di 4 unità → evocare la meno costosa disponibile dalla mano
  6. Altrimenti, muoversi verso il bersaglio (casella di movimento più vicina al nemico)
  7. Se abbastanza mana (> 2), dash (doppio movimento) per avvicinarsi ancora
  8. Se l'unità è l'Oracle → scappa
mermaid diagram
async makeAction(dash=false) {
    // tutto in sequenza
    // il CPU dash se ha abbastanza mana
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Perché la distanza euclidea

Uso la distanza euclidea:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Perché non Manhattan? Perché le unità hanno pattern di movimento vari (a L come il cavallo, diagonale, ecc.). La distanza in linea d'aria è un'approssimazione migliore del pericolo.

Perché non minimax

Avrei potuto fare un minimax classico. Ma con 12 tipi di unità, pattern di movimento diversi, abilità speciali... l'albero di gioco esplode così in fretta che diventa ingiocabile. L'approccio euristico fa scelte intelligenti senza esplorare 10 milioni di stati.

Cosa è figo

Il sistema di attrattività crea dilemmi divertenti:

  • Il Veggente (70) genera mana. Se lo lasci vivere, l'avversario ha più risorse. Ma il Titano (95) è ancora più pericoloso.
  • Il Mutforma (90) può scambiarsi di posto con qualsiasi unità. Può rubarti l'Oracle.
  • L'Arpia (50) ha un attacco esplosivo che uccide anche lei. Non prioritaria... finché non è a fianco di 3 delle tue unità.

L'IA valuta il pericolo globale in base alle posizioni, non solo le stats grezze.

C'è anche una funzione activateSimulation() per testare scenari senza rifare una partita:

activateSimulation() {
    // Piazza unità specifiche sulla plancia
    // Utile per debuggare l'IA
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Cosa manca

Se avessi avuto più tempo:

  • L'IA reagisce allo stato attuale, non prevede cosa farà il giocatore
  • Non pianifica la mano su più turni
  • Il Mutforma e il Centauro hanno abilità che sottoutilizza
  • Apprendimento per rinforzo: farla giocare contro sé stessa per aggiustare i coefficienti

Ma per un gioco da browser funziona. Dei miei amici riescono a perderci contro, quindi siamo a posto xD

Prova

Disponibile su nausicaa-game.github.io. Clicchi "GIOCA", CPU mode ON, e guardi l'IA fare.

Consiglio: lascia l'IA giocare contro sé stessa. Vedrai fasi aggressive, poi pfft indietreggia tutto.

Il codice è su GitHub in js/cpu.js.

3 cose:

  1. Coefficienti euristici -- niente minimax, ogni unità ha un'attrattività
  2. Coefficienti che cambiano ogni 5 turni -- l'IA alterna aggressività e controllo, stile Pac-Man
  3. L'Oracle scappa -- calcola la direzione opposta alla minaccia e se la squaglia

Se hai idee per rendere l'IA ancora più bastarda, apri una issue. Ho dei piani per una versione che impara dalle sconfitte, ma sarà per il prossimo articolo xD

Meine bescheuerte KI für Nausicaa

Eine heuristische KI mit Koeffizienten, Hyperparametern die sich

Meine bekloppte KI für Nausicaa

Es gibt Projekte, die fangen an mit "hey, was wenn ich ein Schachspiel mit Mythologien mach?" und enden mit einem Ding, bei dem eine KI alle 5 Runden ihre eigenen Hyperparameter neu würfelt.

Nausicaa ist genau das. Ein rundenbasiertes Brettspiel, wo du dein Deck aus mythischen Kreaturen baust, dein Mana managst und Einheiten auf einem 10x8-Brett platzierst. Und dann ist da eine KI mit Persönlichkeitsstörungen.

Ich hab ziemlich viel Zeit in diese KI gesteckt, und das Ergebnis ist ziemlich unberechenbar xD

Das Spiel in echt

Bevor ich über das Gehirn rede, musst du den Körper verstehen:

  • 10x8-Brett, 2 Reihen Einsatzzone pro Spieler
  • Mana startet bei 1, +1 pro Runde, max 6. Du bezahlst damit für Beschwörungen, Angriffe und Fähigkeiten
  • Ziel : den gegnerischen Oracle weghauen

12 Einheiten, unterschiedliche Kosten und Bewegungsmuster:

Unit Kosten Bewegung HP
Oracle 0 König (8 Richtungen) 1
Goblin 1 3 Felder vorwärts 1
Harpyie 1 König (8 Richtungen) 1
Najade 1 Diagonale 1
Greif 2 2 Felder hüpfen 2
Sirene 2 Seitwärts 1
Zentaur 2 Springer (L-Form) 2
Bogenschütze 3 Seitwärts 1
Phönix 3 Diagonale (dunkle Felder) 1
Gestaltwandler 4 Platz tauschen 1
Seher 4 Keine (generiert Mana) 1
Titan 6 Eingeschränkt (Flächenangriff) 3

Jede Einheit hat ihr eigenes Angriffsmuster. Die Sirene haut in alle 4 Diagonalen, der Bogenschütze feuert aus 3 Feldern Entfernung, der Titan zerlegt bei Beschwörung alles um sich rum. Kurz gesagt: Schach mit Mytogedöns und Deckbau xD

Wie ich der CPU das Denken beigebracht hab

Die Grundidee ist lächerlich einfach: Jede gegnerische Einheit hat einen Attraktivitätsfaktor. Je gefährlicher sie ist, desto mehr will die KI sich um sie kümmern.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle auf 100 -- logisch, ist die Win-Bedingung. Titan auf 95, weil er bei Beschwörung alles in der Nähe oneshotted. Goblin auf 20, ist ein Fußsoldat, wen juckt's.

Dann für jedes Paar (eine eigene, eine feindliche Einheit) berechne ich:

interesse = attraktivitaet × coeff_attraktiv / (distanz × coeff_distanz)

Im Klartext: Je gefährlicher und näher du bist, desto mehr will die KI dich weghämmern.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

Der Trick mit den wechselnden Koeffizienten

Das Lustige ist: die Gewichtung ändert sich alle 5 Runden zufällig.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Einmal ist die KI hyperaggressiv (Attraktivität 95, Distanz 5), ballert durch alles durch um deinen Oracle zu killen. Nächstes Mal priorisiert sie Distanz und positioniert sich neu.

Das ist von Pac-Mans Geistern geklaut -- Blinky jagt, Pinky lauert auf. Hier wechselt die KI ihre "Persönlichkeit" jede Phase.

Ergebnis: unmöglich, die KI über eine ganze Runde vorherzusagen. Der CPU spielt nie zweimal dasselbe Match.

Der Oracle ist eine Lusche

Der gegnerische Oracle haut ab. Buchstäblich.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Er berechnet die Gegenrichtung zur Bedrohung und macht sich vom Acker. Wenn ne Wand da ist, sucht er das nächste freie Feld in die Richtung.

Du brauchst 3 Runden um an den Oracle ranzukommen, und zack -- er ist abgehauen wie ne kleine Bitch xD

Die Entscheidungsschleife

So entscheidet die KI:

  1. Wenn ich keinen Oracle mehr hab (tot), neuen setzen
  2. Koeffizient für jedes Paar eigene Einheit → feindliche Einheit berechnen
  3. Bestes Paar auswählen
  4. Wenn die Einheit das Ziel von ihrer Position aus angreifen kann → angreifen
  5. Wenn ich weniger als 4 Einheiten hab → günstigste verfügbare aus der Hand beschwören
  6. Sonst: zum Ziel bewegen (Feld, das der Einheit am nächsten ist)
  7. Wenn genug Mana (> 2), Dash (Doppelzug) um noch näher ranzukommen
  8. Wenn die Einheit der Oracle ist → fliehen
mermaid diagram
async makeAction(dash=false) {
    // das Ganze in Sequence
    // der CPU dasht wenn er genug Mana hat
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Warum euklidische Distanz

Ich nutze die euklidische Distanz:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Warum nicht Manhattan? Weil die Einheiten verschiedene Bewegungsmuster haben (L-Form wie der Springer, Diagonale, etc). Die Luftlinie ist ne bessere Annäherung an die Gefahr.

Warum kein Minimax

Ich hätte auch nen klassischen Minimax bauen können. Aber mit 12 Einheitentypen, verschiedenen Bewegungsmustern, Spezialfähigkeiten... der Spielbaum explodiert so dermaßen, dass es unspielbar wird. Der heuristische Ansatz trifft intelligente Entscheidungen ohne 10 Millionen Zustände zu durchforsten.

Was cool ist

Das Attraktivitätssystem erzeugt lustige Dilemmas:

  • Der Seher (70) generiert Mana. Wenn du ihn leben lässt, hat der Gegner mehr Ressourcen. Aber der Titan (95) ist noch gefährlicher.
  • Der Gestaltwandler (90) kann mit jeder Einheit den Platz tauschen. Er kann deinen Oracle klauen.
  • Die Harpyie (50) hat einen explosiven Angriff, der sie selbst tötet. Nicht prio... bis sie neben 3 deiner Einheiten steht.

Die KI bewertet die globale Gefahr anhand der Positionen, nicht nur der Roh-Statuswerte.

Es gibt auch activateSimulation() um Szenarien zu testen, ohne ne ganze Runde zu spielen:

activateSimulation() {
    // Platziert bestimmte Einheiten auf dem Brett
    // Nützlich zum Debuggen der KI
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Was fehlt

Wenn ich mehr Zeit gehabt hätte:

  • Die KI reagiert nur auf den aktuellen Zustand, sie sagt nicht voraus, was der Spieler macht
  • Sie plant ihre Hand nicht über mehrere Runden
  • Der Gestaltwandler und der Zentaur haben Fähigkeiten, die sie unternutzt
  • Reinforcement Learning: sie gegen sich selbst spielen lassen um die Koeffizienten zu optimieren

Aber für ein Browserspiel reicht's. Kumpels schaffen es dagegen zu verlieren, also ist gut xD

Test es selbst

Verfügbar auf nausicaa-game.github.io. Klick auf "JOUER", CPU mode ON, und schau der KI zu.

Tipp: lass die KI gegen sich selbst spielen. Du siehst aggressive Phasen, und dann -- puff -- zieht sie sich komplett zurück.

Der Code liegt auf GitHub in js/cpu.js.

3 Takeaways:

  1. Heuristische Koeffizienten -- kein Minimax, jede Einheit hat eine Attraktivität
  2. Koeffizienten wechseln alle 5 Runden -- die KI wechselt zwischen Aggression und Kontrolle, Pac-Man-Style
  3. Der Oracle flieht -- er berechnet die Gegenrichtung zur Bedrohung und macht sich vom Acker

Wenn du Ideen hast, um die KI noch fieser zu machen, mach ein Issue auf. Ich hab Pläne für ne Version, die aus ihren Niederlagen lernt, aber das kommt in nem anderen Artikel xD

Мой тупой ИИ для Nausicaa

Эвристический ИИ на коэффициентах, гиперпараметры которых меняются

Моя долбанутая ИИ для Nausicaa

Бывают проекты, которые начинаются с "а что если сделать шахматы с мифологией?", а заканчиваются какой-то ИИ, которая меняет свои гиперпараметры каждые 5 ходов.

Nausicaa -- это оно. Пошаговая настолка, где ты собираешь колоду мифических существ, управляешь маной, выставляешь юнитов на поле 10x8. А ещё там ИИ с раздвоением личности.

Я потратил дофига времени на эту ИИ, и результат просто неуправляемый xD

Что за игра

Прежде чем говорить про мозги, надо понять тело:

  • Поле 10x8, зона размещения по 2 ряда на игрока
  • Мана стартует с 1, +1 за ход, макс 6. Тратишь на призыв, атаку, способности
  • Цель: завалить вражеского Оракула

12 юнитов, разные стоимости и паттерны движения:

Unit Цена Движение HP
Oracle 0 Король (8 направлений) 1
Gobelin 1 Вперёд 3 клетки 1
Harpie 1 Король (8 направлений) 1
Naïade 1 Диагональ 1
Griffin 2 Прыжок на 2 клетки 2
Sirène 2 Боком 1
Centaure 2 Конём (буквой Г) 2
Archer 3 Боком 1
Phénix 3 Диагональ (тёмные клетки) 1
Métamorphe 4 Обмен местами 1
Voyant 4 Никуда (генерит ману) 1
Titan 6 Ограниченно (атака по зоне) 3

У каждого юнита свой паттерн атаки. Сирена бьёт по 4 диагоналям, Арчер -- с дистанции на 3 клетки, Титан уничтожает всё вокруг при призыве. Короче, шахматы с мифологией и декбилдингом xD

Как я заставил CPU думать

Идея до примитивности проста: у каждого вражеского юнита есть коэффициент привлекательности. Чем он опаснее, тем больше ИИ хочет им заняться.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Оракул на 100 -- логично, это вин-кондишн. Титан на 95, потому что он ваншотит всё вокруг при призыве. Гоблин на 20 -- пешка, пофиг на него.

Дальше для каждой пары юнитов (свой + вражеский) я считаю:

интерес = привлекательность × коэфф_привл / (расстояние × коэфф_дист)

Короче: чем ты опаснее и ближе, тем больше ИИ хочет тебя размазать.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

Фишка с меняющимися коэффициентами

Прикол в том, что коэффициенты важности рандомно меняются каждые 5 ходов.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

В одном ходу ИИ сверхагрессивна (аттрактивность 95, дистанция 5) -- прёт через всё, чтоб завалить твоего Оракула. В следующем приоритет -- дистанция, и она перестраивается.

Это украдено у призраков из Pac-Man -- Blinky преследует, Pinky ставит засады. Тут ИИ меняет "личность" каждую фазу.

Результат: невозможно предсказать ИИ на всю партию. CPU ни разу не проводит два одинаковых матча.

Оракул -- ссыкло

Вражеский Оракул драпает. Буквально.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Он вычисляет направление от угрозы и сваливает. Если упёрся в стену -- ищет ближайшую свободную клетку в ту же сторону.

Ты 3 хода подбираешься к Оракулу, а он сваливает как трусишка xD

Цикл принятия решений

Вот как ИИ решает:

  1. Если Оракула больше нет (убили) -- поставить нового
  2. Посчитать коэффициент для каждой пары свой юнит → вражеский юнит
  3. Выбрать лучшую пару
  4. Если юнит может атаковать цель с текущей позиции -- атакует
  5. Если меньше 4 юнитов -- призвать самого дешёвого из руки
  6. Иначе -- двигаться к цели (клетка движения ближе всего к врагу)
  7. Если маны дохера (> 2) -- даш (двойное движение) чтоб подобраться ещё ближе
  8. Если юнит -- Оракул -- бежать
mermaid diagram
async makeAction(dash=false) {
    // всё это по очереди
    // CPU делает даш если маны дохера
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Почему евклидово расстояние

Я использую евклидово расстояние:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Почему не Манхэттен? Потому что юниты двигаются по-разному (буквой Г как конь, по диагонали и т.д.). Расстояние по прямой лучше оценивает угрозу.

Почему не минимакс

Я мог бы закодить классический минимакс. Но с 12 типами юнитов, разными паттернами движения, особыми способностями... дерево игры взрывается так быстро, что становится неиграбельно. Эвристический подход принимает умные решения без перебора 10 миллионов состояний.

Что крутого

Система привлекательности создаёт забавные дилеммы:

  • Voyant (70) генерит ману. Оставишь его жить -- у противника больше ресурсов. Но Titan (95) ещё опаснее.
  • Métamorphe (90) может меняться местами с любым юнитом. Способен украсть твоего Оракула.
  • Harpie (50) имеет взрывную атаку, которая убивает и её саму. Не приоритет... пока она не оказалась рядом с тремя твоими юнитами.

ИИ оценивает общую опасность по позициям, а не просто по голым статам.

Ещё есть функция activateSimulation() для теста сценариев без новой партии:

activateSimulation() {
    // Ставит конкретных юнитов на поле
    // Удобно для отладки ИИ
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Чего не хватает

Было бы больше времени:

  • ИИ реагирует на текущее состояние, а не предсказывает действия игрока
  • Не планирует руку на несколько ходов вперёд
  • Métamorphe и Centaure имеют способности, которые она недоиспользует
  • Обучение с подкреплением: пусть играет сама с собой, чтоб настроить коэффициенты

Но для браузерной игры норм. Друзья умудряются проигрывать ей, так что всё ок xD

Попробуй

Доступно на nausicaa-game.github.io. Тыкаешь "JOUER", CPU mode ON, и смотришь как ИИ делает дела.

Совет: дай ИИ поиграть сама с собой. Увидишь агрессивные фазы, а потом -- бац, она вся отступает.

Код на GitHub в js/cpu.js.

3 штуки:

  1. Эвристические коэффициенты -- без минимакса, у каждого юнита своя привлекательность
  2. Коэффициенты меняются каждые 5 ходов -- ИИ чередует агрессию и контроль, как в Pac-Man
  3. Оракул драпает -- вычисляет направление от угрозы и валит

Если есть идеи, как сделать ИИ ещё подлее -- открывай issue. У меня планы на версию, которая учится на своих поражениях, но это уже в следующей статье xD

Mi puta IA para Nausicaä

Una IA basada en coeficientes heurísticos, hiperparámetros que

Mi puta IA para Nausicaä

Hay proyectos que empiezan con "oye, ¿y si hiciera un juego de ajedrez con mitologías?" y terminan con una IA que se cambia sus propios hiperparámetros cada 5 turnos.

Nausicaä es eso. Un juego de mesa por turnos donde armas tu mazo de criaturas mitológicas, gestionas tu maná, desplegás unidades en un tablero 10x8. Y hay una IA que tiene crisis de personalidad.

Le metí bastante tiempo a esta IA, y el resultado es bastante ingobernable xD

El juego en sí

Antes de hablar del cerebro, hay que entender el cuerpo:

  • Tablero 10x8, zona de despliegue de 2 filas por jugador
  • Maná empieza en 1, +1 por turno, máximo 6. Lo gastás para invocar, atacar, usar habilidades
  • Objetivo: reventar al Oraculo enemigo

12 unidades, costes y patrones de movimiento distintos:

Unidad Coste Movimiento PV
Oráculo 0 Rey (8 direcciones) 1
Goblin 1 Adelante 3 casillas 1
Arpía 1 Rey (8 direcciones) 1
Náyade 1 Diagonal 1
Grifo 2 Salta 2 casillas 2
Sirena 2 Lateral 1
Centauro 2 Caballo (en L) 2
Arquero 3 Lateral 1
Fénix 3 Diagonal (casillas oscuras) 1
Metamorfo 4 Intercambio de lugar 1
Vidente 4 Ninguno (genera maná) 1
Titán 6 Limitado (ataque en área) 3

Cada unidad tiene su propio patrón de ataque. La Sirena golpea en las 4 diagonales, el Arquero a distancia 3 casillas, el Titán destruye todo alrededor al invocarse. En resumen, un ajedrez con mitología y deckbuilding xD

Cómo hice pensar a la CPU

La idea base es ridículamente simple: cada unidad enemiga tiene un coeficiente de atractividad. Cuanto más peligrosa es, más quiere la IA ocuparse de ella.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oráculo en 100 -- lógico, es la win condition. Titán en 95 porque revienta todo lo que tenga al lado al invocarse. Goblin en 20, es un soldado raso, nos la suda.

Después, para cada par de unidades (una aliada, una enemiga), calculo:

interes = atractividad × coeff_atract / (distancia × coeff_dist)

Básicamente: mientras más peligroso y cerca estés, más te quiere partir la cara la IA.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

El truco de los coeficientes que cambian

Lo divertido es que los coeficientes de importancia cambian aleatoriamente cada 5 turnos.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Un turno la IA va hiper agresiva (atract 95, distancia 5), atraviesa todo para reventar a tu Oráculo. Al siguiente prioriza la distancia y se recoloca.

Está sacado de los fantasmas de Pac-Man -- Blinky caza, Pinky embosca. Acá la IA cambia de "personalidad" cada fase.

Resultado: imposible predecir a la IA en una partida entera. La CPU nunca hace dos veces la misma partida.

El Oráculo es un cagón

El Oráculo enemigo huye. Literalmente.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Calcula la dirección opuesta a la amenaza y se piira. Si hay pared, busca la casilla libre más cercana en esa dirección.

Pasás 3 turnos acercándote al Oráculo, y puf se fue como una nenita xD

El loop de decisión

Así decide la IA:

  1. Si ya no tengo Oráculo (muerto), colocar uno nuevo
  2. Calcular el coeficiente para cada par unidad aliada → unidad enemiga
  3. Elegir el mejor par
  4. Si la unidad puede atacar al objetivo desde su posición → ataca
  5. Si tengo menos de 4 unidades → invocar la más barata disponible desde la mano
  6. Sino, moverse hacia el objetivo (casilla de movimiento más cercana al enemigo)
  7. Si tengo suficiente maná (> 2), dash (doble movimiento) para acercarse más
  8. Si la unidad es el Oráculo → huir
mermaid diagram
async makeAction(dash=false) {
    // todo eso en secuencia
    // la CPU hace dash si tiene suficiente maná
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Por qué distancia euclidiana

Uso distancia euclidiana:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

¿Por qué no Manhattan? Porque las unidades tienen patrones de movimiento variados (en L como el caballo, diagonal, etc). La distancia en línea recta es una mejor aproximación del peligro.

Por qué no minimax

Podría haber hecho un minimax clásico. Pero con 12 tipos de unidades, patrones de movimiento distintos, habilidades especiales... el árbol de juego explota tan rápido que se vuelve injugable. El enfoque heurístico toma decisiones inteligentes sin explorar 10 millones de estados.

Lo que mola

El sistema de atractividad crea dilemas divertidos:

  • El Vidente (70) genera maná. Si lo dejás vivir, el rival tiene más recursos. Pero el Titán (95) es más peligroso aún.
  • El Metamorfo (90) puede intercambiar su lugar con cualquier unidad. Puede robarte el Oráculo.
  • La Arpía (50) tiene un ataque explosivo que también la mata a ella. No es prioritaria... hasta que está al lado de 3 de tus unidades.

La IA evalúa el peligro global según las posiciones, no solo las stats brutas.

También hay una función activateSimulation() para probar escenarios sin rehacer una partida:

activateSimulation() {
    // Coloca unidades específicas en el tablero
    // Útil para debuguear la IA
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Lo que falta

Si tuviera más tiempo:

  • La IA reacciona al estado actual, no predice lo que hará el jugador
  • No planifica su mano a varios turnos
  • El Metamorfo y el Centauro tienen habilidades que infrautiliza
  • Aprendizaje por refuerzo: hacer que juegue contra sí misma para ajustar los coeficientes

Pero para un juego de navegador cumple. Colegas llegan a perder contra ella, así que está bien xD

Pruébalo

Disponible en nausicaa-game.github.io. Le das a "JOUER", CPU mode ON, y ves a la IA en acción.

Consejo: deja que la IA juegue contra sí misma. Vas a ver fases agresivas, y de repente puf, se retira todo.

El código está en GitHub en js/cpu.js.

3 claves:

  1. Coeficientes heurísticos -- nada de minimax, cada unidad tiene una atractividad
  2. Coeficientes que cambian cada 5 turnos -- la IA alterna agresividad y control, estilo Pac-Man
  3. El Oráculo huye -- calcula la dirección opuesta a la amenaza y se larga

Si tenés ideas para hacer la IA más viciosa, abrí un issue. Tengo planes para una versión que aprende de sus derrotas, pero eso será para otro artículo xD

Minha IA idiota para Nausicaa

Uma IA com coeficientes heurísticos, hiperparâmetros que mudam a

Minha IA idiota para Nausicaa

Tem projetos que começam com "e se eu fizesse um jogo de xadrez com mitologias?" e terminam com uma IA que decide seus próprios hiperparâmetros a cada 5 turnos.

Nausicaa é isso. Um jogo de tabuleiro em turnos onde você constrói seu deck de criaturas mitológicas, gerencia seu mana, implanta unidades em um tabuleiro 10x8. E tem uma IA que tem crises de personalidade.

Passei um bom tempo nessa IA, e o resultado é bem ingerenciável xD

O jogo de verdade

Antes de falar do cérebro, precisa entender o corpo:

  • Tabuleiro 10x8, zona de implantação de 2 fileiras por jogador
  • Mana começa em 1, +1 por turno, máx 6. Você gasta para invocar, atacar, usar habilidades
  • Objetivo: matar o Oráculo adversário

12 unidades, custos e padrões de movimento diferentes:

Unit Custo Movimento PV
Oráculo 0 Rei (8 direções) 1
Goblin 1 Frente 3 casas 1
Harpia 1 Rei (8 direções) 1
Náiade 1 Diagonal 1
Grifo 2 Pular 2 casas 2
Sereia 2 Lateral 1
Centauro 2 Cavalo (em L) 2
Arqueiro 3 Lateral 1
Fênix 3 Diagonal (casas escuras) 1
Metamorfo 4 Troca de lugar 1
Vidente 4 Nenhum (gera mana) 1
Titã 6 Limitado (ataque em área) 3

Cada unidade tem seu próprio padrão de ataque. A Sereia ataca nas 4 diagonais, o Arqueiro à distância em 3 casas, o Titã destrói tudo ao redor na invocação. Enfim, um jogo de xadrez com mitologias e deckbuilding xD

Como fiz a CPU pensar

A ideia básica é idiotamente simples: cada unidade inimiga tem um coeficiente de atratividade. Quanto mais perigosa, mais a IA quer cuidar dela.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oráculo em 100 -- lógico, é a condição de vitória. Titã em 95 porque ele dá OS em tudo ao lado na invocação. Goblin em 20, é um soldado raso, tanto faz.

Em seguida, para cada par de unidades (uma aliada, uma inimiga), calculo:

interesse = atratividade × coeff_atrat / (distância × coeff_dist)

Resumindo: quanto mais perigoso e próximo, mais a IA quer te destruir.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

A jogada dos coeficientes que mudam

O engraçado é que os coeficientes de importância mudam aleatoriamente a cada 5 turnos.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Uma hora a IA vai hiper agressiva (atratividade em 95, distância em 5), ela atravessa tudo para matar seu Oráculo. Na próxima ela prioriza a distância e se reposiciona.

É tirado dos fantasmas do Pac-Man -- Blinky persegue, Pinky embosca. Aqui a IA muda de "personalidade" a cada fase.

Resultado: impossível prever a IA durante uma partida inteira. A CPU nunca faz duas vezes a mesma partida.

O Oráculo é um covarde

O Oráculo inimigo foge. Literalmente.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Ele calcula a direção oposta à ameaça e vaza. Se tem uma parede, ele procura a casa livre mais próxima nessa direção.

Você passa 3 turnos se aproximando do Oráculo, e puf ele fugiu que nem um covarde xD

O loop de decisão

Eis como a IA decide:

  1. Se não tenho mais Oráculo (morto), colocar um novo
  2. Calcular o coeficiente para cada par unidade aliada → unidade inimiga
  3. Escolher o melhor par
  4. Se a unidade pode atacar o alvo da sua posição → ataque
  5. Se tenho menos de 4 unidades → invocar a mais barata disponível da mão
  6. Senão, mover-se em direção ao alvo (casa de movimento mais próxima do inimigo)
  7. Se mana suficiente (> 2), dash (movimento duplo) para se aproximar ainda mais
  8. Se a unidade é o Oráculo → fugir
mermaid diagram
async makeAction(dash=false) {
    // tudo isso em sequência
    // a CPU dá dash se tiver mana suficiente
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Por que distância euclidiana

Uso distância euclidiana:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Por que não Manhattan? Porque as unidades têm padrões de movimento variados (L como o cavalo, diagonal, etc). A distância em linha reta é uma melhor aproximação do perigo.

Por que não minimax

Eu poderia ter codificado um minimax clássico. Mas com 12 tipos de unidades, padrões de movimento diferentes, habilidades especiais... a árvore de jogo explode tão rápido que fica injogável. A abordagem heurística faz escolhas inteligentes sem explorar 10 milhões de estados.

O sistema de atratividade cria dilemas engraçados:

  • O Vidente (70) gera mana. Se você deixá-lo vivo, o adversário tem mais recursos. Mas o Titã (95) é ainda mais perigoso.
  • O Metamorfo (90) pode trocar de lugar com qualquer unidade. Ele pode roubar seu Oráculo.
  • A Harpia (50) tem um ataque explosivo que também a mata. Não prioritária... até que ela esteja ao lado de 3 das suas unidades.

A IA avalia o perigo global de acordo com as posições, não apenas as estatísticas brutas.

Tem também uma função activateSimulation() para testar cenários sem refazer uma partida:

activateSimulation() {
    // Coloca unidades específicas no tabuleiro
    // Útil para debuggar a IA
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

O que falta

Se tivesse mais tempo:

  • A IA reage ao estado atual, não prevê o que o jogador vai fazer
  • Ela não planeja a mão em vários turnos
  • O Metamorfo e o Centauro têm habilidades que ela subutiliza
  • Aprendizado por reforço: fazê-la jogar contra si mesma para ajustar os coeficientes

Mas para um jogo de navegador cumpre o papel. Uns amigos conseguem perder pra ela, então tá bom xD

Teste

Disponível em nausicaa-game.github.io. Você clica em "JOGAR", CPU mode ON, e assiste a IA agir.

Dica: deixe a IA jogar contra si mesma. Você vai ver fases agressivas, e puf ela recua tudo.

O código está no GitHub em js/cpu.js.

3 coisas:

  1. Coeficientes heurísticos -- sem minimax, cada unidade tem uma atratividade
  2. Coeficientes que mudam a cada 5 turnos -- a IA alterna agressividade e controle, estilo Pac-Man
  3. O Oráculo foge -- ele calcula a direção oposta à ameaça e vaza

Se você tem ideias para deixar a IA ainda mais perversa, abra uma issue. Tenho planos para uma versão que aprende com suas derrotas, mas isso fica para um próximo artigo xD

AI Konyolku untuk Nausicaa

AI dengan koefisien heuristik, hyper-parameter yang berubah setiap 5

AI Konyolku untuk Nausicaa

Ada proyek yang dimulai dengan "bagaimana kalau aku bikin game catur dengan mitologi?" dan berakhir dengan sesuatu yang punya AI yang memutuskan hyper-parameternya sendiri setiap 5 giliran.

Nausicaa seperti itu. Game papan bergiliran di mana kamu membangun deck makhluk mitologis, mengelola mana, dan menempatkan unit di papan 10x8. Dan ada AI yang mengalami krisis kepribadian.

Aku menghabiskan cukup banyak waktu untuk AI ini, dan hasilnya cukup kacau xD

Game yang Sebenarnya

Sebelum bicara soal otak, kita perlu memahami tubuhnya:

  • Papan 10x8, zona penempatan 2 baris per pemain
  • Mana mulai dari 1, +1 per giliran, maks 6. Kamu habiskan untuk memanggil, menyerang, menggunakan kemampuan
  • Tujuan: bunuh Oracle lawan

12 unit, dengan biaya dan pola gerakan berbeda:

Unit Biaya Gerakan HP
Oracle 0 Raja (8 arah) 1
Goblin 1 Maju 3 petak 1
Harpy 1 Raja (8 arah) 1
Naiad 1 Diagonal 1
Griffin 2 Lompat 2 petak 2
Sirene 2 Samping 1
Centaur 2 Kuda (bentuk L) 2
Pemanah 3 Samping 1
Phoenix 3 Diagonal (petak gelap) 1
Shapeshifter 4 Tukar tempat 1
Peramal 4 Tidak ada (menghasilkan mana) 1
Titan 6 Terbatas (serangan area) 3

Setiap unit memiliki pola serangannya sendiri. Sirene menyerang ke 4 diagonal, Pemanah jarak jauh sejauh 3 petak, Titan menghancurkan segala sesuatu di sekitarnya saat dipanggil. Singkatnya game catur dengan mitologi dan deckbuilding xD

Bagaimana Aku Membuat CPU Berpikir

Ide dasarnya sangat sederhana: setiap unit musuh memiliki koefisien daya tarik. Semakin berbahaya, semakin AI ingin mengurusinga.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle 100 -- logis, itu win condition. Titan 95 karena dia OS apa pun di sampingnya saat dipanggil. Goblin 20, itu prajurit biasa, kita tidak peduli.

Lalu untuk setiap pasangan unit (satu sekutu, satu musuh), aku hitung:

interet = attractivite × coeff_attract / (distance × coeff_dist)

Intinya: semakin berbahaya dan dekat, semakin AI ingin menghajarmu.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

Trik Koefisien yang Berubah

Yang lucu adalah koefisien kepentingan berubah secara acak setiap 5 giliran.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Sekali waktu AI akan sangat agresif (daya tarik 95, jarak 5), dia menerobos segalanya untuk membunuh Oracle-mu. Giliran berikutnya dia memprioritaskan jarak dan reposisi.

Ini terinspirasi dari hantu Pac-Man -- Blinky mengejar, Pinky menyergap. Di sini AI berubah "kepribadian" setiap fase.

Hasil: tidak mungkin memprediksi AI dalam satu permainan penuh. CPU tidak pernah melakukan pertandingan yang sama dua kali.

Oracle Itu Pengecut

Oracle lawan kabur. Secara harfiah.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Dia menghitung arah berlawanan dari ancaman dan kabur. Kalau ada dinding, dia cari petak kosong terdekat di arah itu.

Kamu habiskan 3 giliran mendekati Oracle, dan bam dia kabur seperti pengecut xD

Loop Pengambilan Keputusan

Begini cara AI memutuskan:

  1. Jika Oracle sudah mati, tempatkan Oracle baru
  2. Hitung koefisien untuk setiap pasangan unit sekutu → unit musuh
  3. Pilih pasangan terbaik
  4. Jika unit bisa menyerang target dari posisinya → serang
  5. Jika unit kurang dari 4 → panggil yang paling murah dari tangan
  6. Jika tidak, bergerak menuju target (petak gerakan terdekat ke musuh)
  7. Jika mana cukup (> 2), dash (gerakan ganda) untuk mendekat lebih lanjut
  8. Jika unit adalah Oracle → kabur
mermaid diagram
async makeAction(dash=false) {
    // tout ça en séquence
    // le CPU dash si il a assez de mana
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Kenapa Jarak Euclidean

Aku menggunakan jarak Euclidean:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Kenapa bukan Manhattan? Karena unit memiliki pola gerakan yang bervariasi (L seperti kuda, diagonal, dll). Jarak garis lurus adalah perkiraan bahaya yang lebih baik.

Kenapa Bukan Minimax

Aku bisa saja membuat minimax klasik. Tapi dengan 12 jenis unit, pola gerakan berbeda, kemampuan khusus... pohon permainan meledak begitu cepat sehingga tidak bisa dimainkan. Pendekatan heuristik membuat pilihan cerdas tanpa menjelajahi 10 juta state.

Yang Keren

Sistem daya tarik menciptakan dilema lucu:

  • Peramal (70) menghasilkan mana. Kalau kamu biarkan hidup, lawan punya lebih banyak sumber daya. Tapi Titan (95) masih lebih berbahaya.
  • Shapeshifter (90) bisa bertukar tempat dengan unit mana pun. Dia bisa mencuri Oracle-mu.
  • Harpy (50) memiliki serangan eksplosif yang juga membunuhnya. Tidak prioritas... sampai dia berada di samping 3 unit-mu.

AI mengevaluasi bahaya global berdasarkan posisi, bukan hanya stat mentah.

Ada juga fungsi activateSimulation() untuk menguji skenario tanpa memulai permainan baru:

activateSimulation() {
    // Place des unités spécifiques sur le plateau
    // Utile pour debugger l'IA
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Yang Kurang

Kalau aku punya lebih banyak waktu:

  • AI bereaksi terhadap keadaan saat ini, tidak memprediksi apa yang akan dilakukan pemain
  • Dia tidak merencanakan tangan untuk beberapa giliran
  • Shapeshifter dan Centaur memiliki kemampuan yang kurang dimanfaatkan
  • Reinforcement learning: membuatnya bermain melawan dirinya sendiri untuk menyesuaikan koefisien

Tapi untuk game browser, ini sudah cukup. Teman-teman bisa kalah melawannya, jadi ini ok xD

Coba

Tersedia di nausicaa-game.github.io. Kamu klik "MAIN", CPU mode ON, dan lihat AI bekerja.

Saran: biarkan AI bermain melawan dirinya sendiri. Kamu akan melihat fase agresif, lalu tiba-tiba dia mundur semua.

Kode ada di GitHub di js/cpu.js.

3 hal:

  1. Koefisien heuristik -- tanpa minimax, setiap unit memiliki daya tarik
  2. Koefisien berubah setiap 5 giliran -- AI bergantian antara agresif dan kontrol, ala Pac-Man
  3. Oracle kabur -- dia menghitung arah berlawanan dari ancaman dan kabur

Kalau kamu punya ide untuk membuat AI lebih jahat lagi, buka issue. Aku punya rencana untuk versi yang belajar dari kekalahannya, tapi itu untuk artikel berikutnya xD

Nausicaa के लिए मेरा बेवकूफी भरा AI

एक ह्युरिस्टिक गुणांक वाला AI, हर 5 बारी पर बदलने वाले हाइपर-पैरामीटर,

Nausicaa के लिए मेरा बेवकूफी भरा AI

कुछ प्रोजेक्ट "चलो पौराणिक कथाओं के साथ शतरंज बनाते हैं" से शुरू होते हैं और एक ऐसे AI पर खत्म होते हैं जो हर 5 बारी पर अपने खुद के हाइपर-पैरामीटर तय करता है।

Nausicaa ऐसा ही है। एक टर्न-बेस्ड बोर्ड गेम जहाँ तुम पौराणिक प्राणियों का डेक बनाते हो, मैना मैनेज करते हो, और 10x8 बोर्ड पर यूनिट्स तैनात करते हो। और इसमें एक AI है जिसे पर्सनालिटी डिसऑर्डर है।

मैंने इस AI पर काफी समय बिताया, और नतीजा काफी बेकाबू है xD

असली गेम

दिमाग की बात करने से पहले, शरीर को समझना होगा:

  • 10x8 बोर्ड, प्रति खिलाड़ी 2 पंक्तियों का डिप्लॉयमेंट ज़ोन
  • मैना 1 से शुरू, +1 प्रति बारी, अधिकतम 6। इसे बुलाने, हमला करने, क्षमताओं के उपयोग में खर्च करते हो
  • लक्ष्य: दुश्मन के Oracle को मारना

12 यूनिट्स, अलग-अलग लागत और मूवमेंट पैटर्न:

Unit लागत चाल HP
Oracle 0 राजा (8 दिशाएँ) 1
गॉब्लिन 1 3 कोशिकाएँ आगे 1
हार्पी 1 राजा (8 दिशाएँ) 1
नायड 1 विकर्ण 1
ग्रिफिन 2 2 कोशिकाएँ उछल 2
सायरन 2 पार्श्व 1
सेंटॉर 2 घोड़ा (L आकार) 2
आर्चर 3 पार्श्व 1
फीनिक्स 3 विकर्ण (गहरे कोशिकाएँ) 1
आकार बदलने वाला 4 स्थान बदलना 1
सीर 4 कोई नहीं (मैना उत्पन्न करता है) 1
टाइटन 6 सीमित (क्षेत्र आक्रमण) 3

हर यूनिट का अपना अटैक पैटर्न है। सायरन 4 विकर्णों पर हमला करता है, आर्चर दूर से 3 कोशिकाओं पर, टाइटन बुलाए जाने पर आसपास सब नष्ट कर देता है। संक्षेप में पौराणिक कथाओं और डेकबिल्डिंग वाला शतरंज xD

मैंने CPU को कैसे सोचना सिखाया

मूल विचार बेवकूफी भरा सरल है: हर दुश्मन यूनिट का एक आकर्षण गुणांक होता है। जितनी खतरनाक, उतना ही AI उससे निपटना चाहता है।

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle 100 -- समझ में आता है, यही जीत की शर्त है। टाइटन 95 क्योंकि यह बुलाए जाने पर आसपास सब OS (एक हिट में मार) देता है। गॉब्लिन 20, यह पैदल सैनिक है, कोई फर्क नहीं पड़ता।

फिर हर यूनिट जोड़ी (एक सहयोगी, एक दुश्मन) के लिए, मैं गणना करता हूँ:

interet = attractivite × coeff_attract / (distance × coeff_dist)

मोटे तौर पर: जितना खतरनाक और करीब, उतना ही AI तुम्हें मारना चाहता है।

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

गुणांक बदलने का मज़ा

मज़ेदार बात यह है कि महत्व गुणांक हर 5 बारी पर बेतरतीब ढंग से बदलते हैं।

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

एक बार AI बहुत आक्रामक होगा (attract 95, distance 5), वह तुम्हारे Oracle को मारने के लिए सब पार कर जाएगा। अगली बार यह दूरी को प्राथमिकता देगा और फिर से स्थिति लेगा।

यह Pac-Man के भूतों से लिया गया है -- Blinky पीछा करता है, Pinky घात लगाता है। यहाँ AI हर चरण में "व्यक्तित्व" बदलता है।

नतीजा: पूरे गेम में AI का अनुमान लगाना असंभव है। CPU कभी एक जैसा मैच नहीं खेलता।

Oracle एक कायर है

दुश्मन का Oracle भागता है। सचमुच।

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

यह खतरे की विपरीत दिशा निकालता है और भाग जाता है। अगर दीवार है, तो यह उस दिशा में सबसे करीबी खाली कोशिका ढूँढता है।

तुम 3 बारियाँ Oracle के पास जाने में बिताते हो, और धम, वह बिल्ली की तरह भाग गया xD

निर्णय लूप

यहाँ बताया गया है कि AI कैसे निर्णय लेता है:

  1. अगर मेरे पास Oracle नहीं है (मर गया), नया रखो
  2. हर सहयोगी → दुश्मन यूनिट जोड़ी के लिए गुणांक गणना करो
  3. सबसे अच्छी जोड़ी चुनो
  4. अगर यूनिट अपनी स्थिति से लक्ष्य पर हमला कर सकती है → हमला करो
  5. अगर मेरे पास 4 से कम यूनिट्स हैं → हाथ में से सबसे सस्ती उपलब्ध बुलाओ
  6. नहीं तो, लक्ष्य की ओर बढ़ो (दुश्मन के सबसे करीबी चाल कोशिका)
  7. अगर पर्याप्त मैना है (> 2), डैश (दोहरी चाल) और करीब जाओ
  8. अगर यूनिट Oracle है → भागो
mermaid diagram
async makeAction(dash=false) {
    // tout ça en séquence
    // le CPU dash si il a assez de mana
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

यूक्लिडियन दूरी क्यों

मैं यूक्लिडियन दूरी का उपयोग करता हूँ:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

मैनहट्टन क्यों नहीं? क्योंकि यूनिट्स के चाल पैटर्न विविध हैं (घोड़े की तरह L, विकर्ण, आदि)। सीधी दूरी खतरे का बेहतर अनुमान है।

मिनीमैक्स क्यों नहीं

मैं क्लासिक मिनीमैक्स कोड कर सकता था। लेकिन 12 प्रकार की यूनिट्स, अलग-अलग चाल पैटर्न, विशेष क्षमताओं के साथ... गेम ट्री इतना तेज़ी से फैलता है कि यह अव्यावहारिक हो जाता है। ह्युरिस्टिक दृष्टिकोण 10 मिलियन स्थितियों की खोज किए बिना बुद्धिमान विकल्प चुनता है।

क्या अच्छा है

आकर्षण प्रणाली मज़ेदार दुविधाएँ पैदा करती है:

  • सीर (70) मैना उत्पन्न करता है। अगर तुम इसे जीने दोगे, प्रतिद्वंद्वी के पास अधिक संसाधन होंगे। लेकिन टाइटन (95) और भी खतरनाक है।
  • आकार बदलने वाला (90) किसी भी यूनिट के साथ अपनी जगह बदल सकता है। यह तुम्हारा Oracle चुरा सकता है।
  • हार्पी (50) का विस्फोटक हमला है जो इसे भी मार देता है। प्राथमिकता नहीं... जब तक यह तुम्हारी 3 यूनिट्स के बगल में न हो।

AI सिर्फ कच्चे आँकड़ों के अनुसार नहीं, बल्कि स्थितियों के अनुसार वैश्विक खतरे का मूल्यांकन करता है।

पूरा गेम दोबारा खेले बिना परिदृश्य परीक्षण के लिए एक activateSimulation() फ़ंक्शन भी है:

activateSimulation() {
    // Place des unités spécifiques sur le plateau
    // Utile pour debugger l'IA
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

क्या कमी है

अगर मेरे पास और समय होता:

  • AI वर्तमान स्थिति पर प्रतिक्रिया करता है, यह भविष्यवाणी नहीं करता कि खिलाड़ी क्या करेगा
  • यह कई बारियों के लिए अपने हाथ की योजना नहीं बनाता
  • आकार बदलने वाला और सेंटॉर ऐसी क्षमताएँ रखते हैं जिनका यह कम उपयोग करता है
  • सुदृढ़ीकरण सीखना: गुणांक समायोजित करने के लिए इसे खुद के खिलाफ खेलने देना

लेकिन ब्राउज़र गेम के लिए यह काम करता है। दोस्त इसके खिलाफ हारने में कामयाब हो जाते हैं, तो ठीक है xD

परीक्षण करो

उपलब्ध है nausicaa-game.github.io. "JOUER" पर क्लिक करो, CPU mode ON, और AI को करते देखो।

सलाह: AI को खुद के खिलाफ खेलने दो। तुम आक्रामक चरण देखोगे, फिर पूफ वह पीछे हट जाता है।

कोड GitHub पर js/cpu.js में है।

3 मुख्य बातें:

  1. ह्युरिस्टिक गुणांक -- कोई मिनीमैक्स नहीं, हर यूनिट का आकर्षण है
  2. हर 5 बारी पर बदलने वाले गुणांक -- AI Pac-Man शैली में आक्रामकता और नियंत्रण के बीच बदलता है
  3. Oracle भागता है -- यह खतरे की विपरीत दिशा निकालता है और भाग जाता है

अगर तुम्हारे पास AI को और खतरनाक बनाने के विचार हैं, तो एक issue खोलो। मेरे पास एक ऐसे संस्करण की योजना है जो अपनी हार से सीखता है, लेकिन वह अगले लेख के लिए होगा xD

ذكائي الاصطناعي التافه من أجل Nausicaa

ذكاء اصطناعي بمعاملات استدلالية، معلمات فائقة تتغير كل 5 أدوار،

ذكائي الاصطناعي التافه من أجل Nausicaa

هناك مشاريع تبدأ بـ "ماذا لو صنعت لعبة شطرنج بالأساطير؟" وتنتهي بشيء يملك ذكاءً اصطناعياً يقرر معاملاته الفائقة بنفسه كل 5 أدوار.

Nausicaa هي هذا. لعبة رقعة تعتمد على الأدوار حيث تبني مجموعة كائناتك الأسطورية، وتدير مانا (mana) خاصتك، وتنشر الوحدات على رقعة 10×8. ولديها ذكاء اصطناعي يعاني من نوبات انفصام في الشخصية.

قضيت وقتاً لا بأس به على هذا الذكاء الاصطناعي، والنتيجة لا تُطاق ×د

اللعبة في الحقيقة

قبل التحدث عن الدماغ، يجب فهم الجسد:

  • رقعة 10×8، منطقة نشر من صفين لكل لاعب
  • المانا يبدأ من 1، +1 كل دور، حد أقصى 6. تنفقه لاستدعاء، مهاجمة، استخدام قدرات
  • الهدف: القضاء على أوراكل (Oracle) الخصم

12 وحدة، تكاليف وأنماط حركة مختلفة:

Unit Cost Movement HP
Oracle 0 King (8 directions) 1
Gobelin 1 Forward 3 squares 1
Harpy 1 King (8 directions) 1
Naiad 1 Diagonal 1
Griffin 2 Jump 2 squares 2
Siren 2 Lateral 1
Centaur 2 Knight (L-shape) 2
Archer 3 Lateral 1
Phoenix 3 Diagonal (dark squares) 1
Shapeshifter 4 Swap places 1
Seer 4 None (generates mana) 1
Titan 6 Limited (area attack) 3

لكل وحدة نمط هجوم خاص بها. الحورية (Siren) تضرب في الأقطار الأربعة، الرامي (Archer) عن بعد على 3 خانات، التايتن (Titan) يدمر كل شيء حوله عند الاستدعاء. باختصار لعبة شطرنج بمخلوقات أسطورية وبناء مجموعات ×د

كيف جعلت المعالج يفكر

الفكرة الأساسية بسيطة بشكل سخيف: لكل وحدة معادية معامل جاذبية. كلما كانت أكثر خطورة، كلما أراد الذكاء الاصطناعي التعامل معها.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

الأوراكل 100 -- منطقي، هو شرط الفوز. التايتن 95 لأنه يدمر كل شيء مجاور عند الاستدعاء. القوبلين 20، جندي مشاة، لا يهم.

ثم لكل زوج من الوحدات (واحدة حليفة وواحدة معادية)، أحسب:

interest = attractiveness × coeff_attract / (distance × coeff_dist)

باختصار: كلما كنت خطيراً وقريباً، كلما أراد الذكاء الاصطناعي تحطيمك.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

خدعة المعاملات المتغيرة

المضحك أن معاملات الأهمية تتغير عشوائياً كل 5 أدوار.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

مرة يصبح الذكاء الاصطناعي عدوانياً جداً (جاذبية 95، مسافة 5)، يعبر كل شيء ليقضي على أوراكلك. المرة التالية يعطي أولوية للمسافة ويعيد التموضع.

هذا مقتبس من أشباح Pac-Man -- Blinky يطارد، Pinky يكمُن. هنا الذكاء الاصطناعي يغير "شخصيته" كل مرحلة.

النتيجة: من المستحيل توقع الذكاء الاصطناعي خلال مباراة كاملة. المعالج لا يكرر نفس المباراة أبداً.

الأوراكل جبان

الأوراكل المعادي يهرب. حرفياً.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

يحسب الاتجاه المعاكس للتهديد ويهرب. إذا وجد جداراً، يبحث عن أقرب خانة فارغة في ذلك الاتجاه.

تقضي 3 أدوار تقترب من الأوراكل، وفجأة يهرب كالجبان ×د

حلقة القرار

هكذا يقرر الذكاء الاصطناعي:

  1. إذا لم يعد لدي أوراكل (مات)، أضع أوراكلاً جديداً
  2. حساب المعامل لكل زوج وحدة حليفة → وحدة معادية
  3. اختيار أفضل زوج
  4. إذا كانت الوحدة تستطيع مهاجمة الهدف من موقعها → هاجم
  5. إذا كان لدي أقل من 4 وحدات → استدع الأرخص المتاح من اليد
  6. وإلا، تحرك نحو الهدف (أقرب خانة حركة للعدو)
  7. إذا كان المانا كافياً (>2)، انقضاض (حركة مزدوجة) للاقتراب أكثر
  8. إذا كانت الوحدة هي الأوراكل → اهرب
mermaid diagram
async makeAction(dash=false) {
    // كل هذا بالتسلسل
    // المعالج ينقض إذا كان لديه مانا كافٍ
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

لماذا المسافة الإقليدية

أستخدم المسافة الإقليدية:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

لماذا لا منهاتن؟ لأن الوحدات لديها أنماط حركة متنوعة (L مثل الحصان، قطري، إلخ). المسافة في خط مستقيم هي تقريب أفضل للخطر.

لماذا ليس minimax

كان بإمكاني كتابة minimax تقليدي. لكن مع 12 نوعاً من الوحدات، أنماط حركة مختلفة، قدرات خاصة... شجرة اللعبة تنفجر بسرعة لدرجة أنها تصبح غير قابلة للعب. المنهج الاستدلالي يتخذ خيارات ذكية دون استكشاف 10 ملايين حالة.

ما هو رائع

نظام الجاذبية يخلق معضلات مضحكة:

  • الرائي (Seer) بقيمة 70 يولّد مانا. إذا تركته يعيش، الخصم لديه موارد أكثر. لكن التايتن (95) لا يزال أكثر خطورة.
  • متغير الشكل (Shapeshifter) بقيمة 90 يمكنه مبادلة مكانه مع أي وحدة. يمكنه سرقة أوراكلك.
  • الهاربي (Harpy) بقيمة 50 لديها هجوم انفجاري يقتلها أيضاً. ليست أولوية... حتى تصبح بجانب 3 من وحداتك.

الذكاء الاصطناعي يقيم الخطر العام حسب المواقع، وليس فقط الإحصائيات الخام.

هناك أيضاً دالة activateSimulation() لاختبار سيناريوهات دون إعادة مباراة:

activateSimulation() {
    // يضع وحدات محددة على الرقعة
    // مفيد لتصحيح الذكاء الاصطناعي
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

ما ينقص

لو كان لدي المزيد من الوقت:

  • الذكاء الاصطناعي يتفاعل مع الحالة الحالية، لا يتنبأ بما سيفعله اللاعب
  • لا يخطط ليده على عدة أدوار
  • متغير الشكل والقنطور (Centaur) لديهما قدرات لا يستغلهما جيداً
  • التعلم المعزز: جعله يلعب ضد نفسه لضبط المعاملات

لكن كلعبة متصفح، يؤدي المهم. بعض الأصدقاء يخسرون أمامه، إذن هو جيد ×د

جربها

متاحة على nausicaa-game.github.io. تضغط على "JOUER"، وضع المعالج ON، وتشاهد الذكاء الاصطناعي وهو يفعل.

نصيحة: دع الذكاء الاصطناعي يلعب ضد نفسه. سترى مراحل عدوانية، ثم فجأة يتراجع كل شيء.

الكود على GitHub في js/cpu.js.

3 أشياء:

  1. معاملات استدلالية -- لا minimax، كل وحدة لها جاذبية
  2. معاملات تتغير كل 5 أدوار -- الذكاء الاصطناعي يتبادل بين العدوانية والتحكم، بأسلوب Pac-Man
  3. الأوراكل يهرب -- يحسب الاتجاه المعاكس للتهديد ويجري

إذا كانت لديك أفكار لجعل الذكاء الاصطناعي أكثر دهاءً، افتح issue. لدي خطط لإصدار يتعلم من هزائمه، لكن ذلك سيكون لمقال قادم ×د

AI Nhảm Nhí Của Tôi Cho Nausicaa

Một AI với hệ số heuristic, các siêu tham số thay đổi mỗi 5 lượt,

AI Nhảm Nhí Của Tôi Cho Nausicaa

Có những dự án bắt đầu bằng "ơ hay mình làm game cờ với thần thoại nhỉ?" và kết thúc bằng một thứ có AI tự quyết định siêu tham số của chính nó mỗi 5 lượt.

Nausicaa là thế đấy. Một game chiến thuật theo lượt nơi bạn xây dựng bộ bài sinh vật thần thoại, quản lý mana, triển khai quân lên bàn cờ 10x8. Và có một AI bị rối loạn đa nhân cách.

Tôi đã dành kha khá thời gian cho cái AI này, và kết quả thì khá là mất kiểm soát xD

Game thực sự ra sao

Trước khi nói về bộ não, cần hiểu cơ thể đã:

  • Bàn cờ 10x8, vùng triển khai 2 hàng mỗi người chơi
  • Mana bắt đầu ở 1, +1 mỗi lượt, tối đa 6. Bạn tiêu để triệu hồi, tấn công, dùng kỹ năng
  • Mục tiêu: hạ Oracle của đối thủ

12 đơn vị, chi phí và pattern di chuyển khác nhau:

Unit Cost Movement HP
Oracle 0 Vua (8 hướng) 1
Gobelin 1 Tiến 3 ô 1
Harpie 1 Vua (8 hướng) 1
Naïade 1 Chéo 1
Griffin 2 Nhảy 2 ô 2
Sirène 2 Ngang 1
Centaure 2 Mã (hình chữ L) 2
Archer 3 Ngang 1
Phénix 3 Chéo (ô tối) 1
Métamorphe 4 Đổi chỗ 1
Voyant 4 Không (sinh mana) 1
Titan 6 Hạn chế (tấn công vùng) 3

Mỗi đơn vị có pattern tấn công riêng. Sirène đánh 4 đường chéo, Archer đánh xa 3 ô, Titan phá hủy mọi thứ xung quanh khi triệu hồi. Nói chung là cờ với thần thoại và xây bài xD

Cách tôi làm cho CPU suy nghĩ

Ý tưởng cơ bản thì ngu một cách đơn giản: mỗi đơn vị địch có một hệ số hấp dẫn. Càng nguy hiểm, AI càng muốn xử lý nó.

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle 100 -- hợp lý, đấy là điều kiện thắng. Titan 95 vì nó one-shot mọi thứ bên cạnh khi triệu hồi. Gobelin 20, chỉ là lính quèn, kệ mẹ nó.

Sau đó với mỗi cặp đơn vị (một đồng minh, một địch), tôi tính:

interet = attractivite × coeff_attract / (distance × coeff_dist)

Nói chung: mày càng nguy hiểm và càng gần, AI càng muốn đập mày.

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

Chiêu trò đổi hệ số

Cái hay là các hệ số quan trọng thay đổi ngẫu nhiên mỗi 5 lượt.

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

Lúc thì AI cực kỳ hung hãn (attract 95, distance 5), nó xông qua mọi thứ để hạ Oracle của mày. Lúc sau nó ưu tiên khoảng cách và tự reposition.

Cái này lấy cảm hứng từ bóng ma Pac-Man -- Blinky rượt đuổi, Pinky phục kích. Ở đây AI đổi "tính cách" mỗi pha.

Kết quả: không thể đoán được AI trong cả ván đấu. CPU không bao giờ chơi hai trận giống nhau.

Oracle là đồ nhát

Oracle địch chạy trốn. Theo nghĩa đen.

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

Nó tính hướng ngược lại với mối đe dọa và chuồn. Nếu gặp tường, nó tìm ô trống gần nhất theo hướng đó.

Mày mất 3 lượt tiếp cận Oracle, và thế là nó chạy mất dép xD

Vòng lặp quyết định

Đây là cách AI quyết định:

  1. Nếu mất Oracle (chết), đặt một Oracle mới
  2. Tính hệ số cho mỗi cặp đơn vị đồng minh → đơn vị địch
  3. Chọn cặp tốt nhất
  4. Nếu đơn vị có thể tấn công mục tiêu từ vị trí hiện tại → tấn công
  5. Nếu có ít hơn 4 đơn vị → triệu hồi đơn vị rẻ nhất có sẵn từ tay bài
  6. Nếu không, di chuyển về phía mục tiêu (ô di chuyển gần địch nhất)
  7. Nếu đủ mana (> 2), dash (di chuyển đôi) để tiến gần hơn
  8. Nếu đơn vị là Oracle → chạy trốn
mermaid diagram
async makeAction(dash=false) {
    // tout ça en séquence
    // le CPU dash si il a assez de mana
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

Tại sao lại là khoảng cách Euclid

Tôi dùng khoảng cách Euclid:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

Sao không dùng Manhattan? Vì các đơn vị có pattern di chuyển đa dạng (chữ L như mã, chéo, v.v.). Khoảng cách đường chim bay là xấp xỉ nguy hiểm tốt hơn.

Sao không dùng minimax

Tôi có thể code minimax cổ điển. Nhưng với 12 loại đơn vị, pattern di chuyển khác nhau, kỹ năng đặc biệt... cây trò chơi nổ tung nhanh đến mức không chơi được. Cách tiếp cận heuristic đưa ra lựa chọn thông minh mà không cần thám hiểm 10 triệu trạng thái.

Cái hay

Hệ thống hấp dẫn tạo ra những tình huống khó xử hài hước:

  • Voyant (70) sinh mana. Nếu để nó sống, đối thủ có thêm tài nguyên. Nhưng Titan (95) thì nguy hiểm hơn nhiều.
  • Métamorphe (90) có thể đổi chỗ với bất kỳ đơn vị nào. Nó có thể cướp Oracle của mày.
  • Harpie (50) có đòn tấn công nổ giết luôn chính nó. Không ưu tiên... cho đến khi nó đứng cạnh 3 đơn vị của mày.

AI đánh giá nguy hiểm tổng thể dựa trên vị trí, không chỉ stats thô.

Cũng có hàm activateSimulation() để test kịch bản mà không cần chơi lại cả ván:

activateSimulation() {
    // Place des unités spécifiques sur le plateau
    // Utile pour debugger l'IA
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

Còn thiếu

Nếu có thêm thời gian:

  • AI chỉ phản ứng với trạng thái hiện tại, không dự đoán người chơi sẽ làm gì
  • Nó không lên kế hoạch cho tay bài nhiều lượt
  • Métamorphe và Centaure có kỹ năng mà AI khai thác chưa hết
  • Học tăng cường: cho nó tự đấu với chính mình để tinh chỉnh hệ số

Nhưng với game trên trình duyệt thì thế là đủ. Bạn bè tôi vẫn thua được, thế là ổn xD

Thử nghiệm

Có sẵn tại nausicaa-game.github.io. Bấm "JOUER", bật CPU mode ON, và xem AI làm việc.

Lời khuyên: để AI tự đấu với nhau. Mày sẽ thấy những pha hung hãn, rồi phút sau nó lùi hết.

Code trên GitHub trong js/cpu.js.

3 điều chính:

  1. Hệ số heuristic -- không minimax, mỗi đơn vị có độ hấp dẫn
  2. Hệ số thay đổi mỗi 5 lượt -- AI chuyển giữa hung hãn và kiểm soát, kiểu Pac-Man
  3. Oracle chạy trốn -- nó tính hướng ngược lại với mối đe dọa và chuồn

Nếu có ý tưởng làm AI đểu hơn nữa, hãy mở issue. Tôi có kế hoạch cho một phiên bản tự học từ thất bại, nhưng để bài sau nhé xD

AI ขี้โกงของฉันสำหรับ Nausicaa

AI ที่ใช้ค่าสัมประสิทธิ์ฮิวริสติก ไฮเปอร์พารามิเตอร์ที่เปลี่ยนทุก

AI ขี้โกงของฉันสำหรับ Nausicaa

มีโปรเจกต์ที่เริ่มจาก "เฮ้ ถ้าฉันทำเกมหมากรุกกับเทพปกรณัมล่ะ" แล้วจบลงด้วย AI ที่ตัดสินใจไฮเปอร์พารามิเตอร์ของตัวเองทุก 5 เทิร์น

Nausicaa ก็เป็นแบบนั้น เกมกระดานผลัดกันเดินที่คุณสร้างเด็คสัตว์ในตำนาน จัดการมานา วางยูนิตบนกระดาน 10x8 และมี AI ที่มีหลายบุคลิกภาพ

ฉันใช้เวลากับ AI นี้พอสมควร และผลลัพธ์ก็ค่อนข้างจะจัดการยาก xD

เกมจริง ๆ

ก่อนจะพูดถึงสมอง ต้องเข้าใจร่างกายก่อน:

  • กระดาน 10x8 พื้นที่วางกำลัง 2 แถวต่อผู้เล่น
  • มานาเริ่มที่ 1 +1 ต่อเทิร์น สูงสุด 6 ใช้จ่ายเพื่ออัญเชิญ โจมตี ใช้ความสามารถ
  • เป้าหมาย: ฆ่า Oracle ของฝ่ายตรงข้าม

12 ยูนิต แต่ละตัวมีต้นทุนและรูปแบบการเดินต่างกัน:

Unit ต้นทุน การเคลื่อนที่ HP
Oracle 0 King (8 ทิศทาง) 1
Gobelin 1 ไปข้างหน้า 3 ช่อง 1
Harpie 1 King (8 ทิศทาง) 1
Naïade 1 แนวทแยง 1
Griffin 2 กระโดด 2 ช่อง 2
Sirène 2 แนวข้าง 1
Centaure 2 อัศวิน (รูปตัว L) 2
Archer 3 แนวข้าง 1
Phénix 3 แนวทแยง (ช่องสีเข้ม) 1
Métamorphe 4 สลับตำแหน่ง 1
Voyant 4 ไม่เคลื่อนที่ (สร้างมานา) 1
Titan 6 จำกัด (โจมตีเป็นพื้นที่) 3

แต่ละยูนิตมีรูปแบบการโจมตีของตัวเอง Sirène โจมตี 4 แนวทแยง Archer ยิงระยะ 3 ช่อง Titan ทำลายทุกอย่างรอบตัวเมื่ออัญเชิญ สั้น ๆ คือหมากรุกผสมเทพปกรณัมและเด็คบิวดิ้ง xD

ฉันทำให้ CPU คิดได้ยังไง

แนวคิดพื้นฐานง่ายมาก: ยูนิตศัตรูแต่ละตัวมีค่าสัมประสิทธิ์ความน่าสนใจ ยิ่งอันตรายมากเท่าไหร่ AI ยิ่งอยากจัดการ

const UNITS_ATTRACTIVENESS = {
    "oracle": 100,
    "titan": 95,
    "shapeshifter": 90,
    "phoenix": 80,
    "siren": 70,
    "archer": 70,
    "seer": 70,
    "griffin": 60,
    "centaur": 60,
    "harpy": 50,
    "naiad": 30,
    "gobelin": 20
};

Oracle 100 -- ก็เหตุผลนะ มันคือเงื่อนไขชนะ Titan 95 เพราะมัน one-shot ทุกอย่างรอบข้างตอนอัญเชิญ Gobelin 20 เป็นแค่ทหารเดินดิน ไม่สน

จากนั้นสำหรับคู่ยูนิตแต่ละคู่ (ฝ่ายเราหนึ่ง ฝ่ายศัตรูหนึ่ง) ผมคำนวณ:

ความสนใจ = ความน่าสนใจ × สัมประสิทธิ์_ความน่าสนใจ / (ระยะทาง × สัมประสิทธิ์_ระยะทาง)

พูดง่าย ๆ คือ ยิ่งอันตรายและใกล้มากเท่าไหร่ AI ยิ่งอยากจัดการ

calculateAttackCoefficient(x1, y1, x2, y2) {
    const distance = this.calculateEuclideanDistance(x1, y1, x2, y2);
    if (distance === 0) return Infinity;
    return (UNITS_ATTRACTIVENESS[unit.type] * COEFFICIENTS_IMPORTANCE["attractiveness"]) / distance;
}

ทริคค่าสัมประสิทธิ์ที่เปลี่ยนไป

ที่สนุกคือค่าสัมประสิทธิ์ความสำคัญ เปลี่ยนแบบสุ่มทุก 5 เทิร์น

if (this.turnCount % 5 === 0) {
    const distanceCoefficient = parseInt(Math.random() * 100);
    const attractivenessCoefficient = parseInt(Math.random() * 100);
    this.regulateImportanceCoefficients({
        distance: distanceCoefficient,
        attractiveness: attractivenessCoefficient
    });
}

รอบนึง AI จะ aggressive สุด (attract 95, distance 5) วิ่งฝ่าทุกอย่างเพื่อฆ่า Oracle รอบถัดไปมันกลับให้ความสำคัญกับระยะทางและ reposition

แนวคิดนี้เอามาจากผีใน Pac-Man -- Blinky ไล่ล่า, Pinky ดักซุ่ม ที่นี่ AI เปลี่ยน "บุคลิกภาพ" ทุกเฟส

ผลลัพธ์: ไม่มีทางคาดเดา AI ได้ตลอดทั้งเกม CPU ไม่เคยเล่นสองครั้งเหมือนกัน

Oracle ขี้ขลาด

Oracle ของศัตรูหนี ตามตัวอักษร

const awayFromTarget = {
    row: Math.max(0, Math.min(7, botUnitElement.row + (botUnitElement.row - targetUnitElement.row))),
    col: Math.max(0, Math.min(9, botUnitElement.col + (botUnitElement.col - targetUnitElement.col)))
};

มันคำนวณทิศทางตรงข้ามกับภัยคุกคามและวิ่งหนี ถ้าเจอกำแพงก็หาช่องว่างที่ใกล้ที่สุดในทิศทางนั้น

ใช้ 3 เทิร์นเดินเข้าไปหา Oracle แล้วปุ๊บมันก็หนีไปแล้ว xD

ลูปการตัดสินใจ

นี่คือวิธีที่ AI ตัดสินใจ:

  1. ถ้าไม่มี Oracle (ตาย) ให้วาง Oracle ใหม่
  2. คำนวณค่าสัมประสิทธิ์สำหรับทุกคู่ยูนิตฝ่ายเรา → ยูนิตศัตรู
  3. เลือกคู่ที่ดีที่สุด
  4. ถ้ายูนิตสามารถโจมตีเป้าหมายจากตำแหน่งปัจจุบันได้ → โจมตี
  5. ถ้ามียูนิตน้อยกว่า 4 ตัว → อัญเชิญยูนิตที่ถูกที่สุดที่มีในมือ
  6. ถ้าไม่ → เคลื่อนที่เข้าหาเป้าหมาย (ช่องเคลื่อนที่ที่ใกล้ศัตรูที่สุด)
  7. ถ้ามีมานาเพียงพอ (> 2) → dash (เคลื่อนที่สองครั้ง) เพื่อเข้าใกล้ยิ่งขึ้น
  8. ถ้ายูนิตคือ Oracle → หนี
mermaid diagram
async makeAction(dash=false) {
    // ทุกอย่างเป็นลำดับ
    // CPU dash ถ้ามีมานาพอ
    if(botPlayer.mana > 2) {
        this.makeAction(true);
    }
}

ทำไมใช้ระยะทางแบบยุคลิด

ผมใช้ระยะทางแบบยุคลิด:

calculateEuclideanDistance(x1, y1, x2, y2) {
    const deltaX = Math.pow(x1 - x2, 2);
    const deltaY = Math.pow(y1 - y2, 2);
    return Math.sqrt(deltaX + deltaY) * COEFFICIENTS_IMPORTANCE["distance"];
}

ทำไมไม่ใช้ Manhattan? เพราะยูนิตมีรูปแบบการเคลื่อนที่หลากหลาย (L แบบอัศวิน, แนวทแยง ฯลฯ) ระยะทางเป็นเส้นตรงประมาณอันตรายได้ดีกว่า

ทำไมไม่ใช้ minimax

ฉันอาจจะเขียน minimax ทั่วไปก็ได้ แต่ด้วยยูนิต 12 แบบ รูปแบบการเคลื่อนที่ต่างกัน ความสามารถพิเศษ... ต้นไม้เกมแตกกิ่งเร็วมากจนเล่นไม่ได้ วิธีฮิวริสติกเลือกอย่างฉลาดโดยไม่ต้องสำรวจหลายสิบล้านสถานะ

สิ่งที่เจ๋ง

ระบบความน่าสนใจสร้างสถานการณ์กลilemmas ที่สนุก:

  • Voyant (70) สร้างมานา ถ้าปล่อยไว้ ฝ่ายตรงข้ามมีทรัพยากรมากขึ้น แต่ Titan (95) ก็อันตรายกว่า
  • Métamorphe (90) สามารถสลับตำแหน่งกับยูนิตใดก็ได้ มันสามารถขโมย Oracle ของคุณได้
  • Harpie (50) มีการโจมตีแบบระเบิดที่ฆ่าตัวเองด้วย ไม่ใช่เป้าหมาย priority... จนกว่ามันจะอยู่ติดกับ 3 ยูนิตของคุณ

AI ประเมินอันตรายโดยรวมตามตำแหน่ง ไม่ใช่แค่สถิติดิบ

นอกจากนี้ยังมีฟังก์ชัน activateSimulation() สำหรับทดสอบสถานการณ์โดยไม่ต้องเล่นใหม่:

activateSimulation() {
    // วางยูนิตเฉพาะบนกระดาน
    // มีประโยชน์สำหรับ debug AI
    this.game.board = simulation.board;
    this.game.players = simulation.players;
}

สิ่งที่ขาดไป

ถ้ามีเวลาเพิ่ม:

  • AI ตอบสนองต่อสถานะปัจจุบัน ไม่ได้ทำนายสิ่งที่ผู้เล่นจะทำ
  • ไม่ได้วางแผนการใช้มือข้ามหลายเทิร์น
  • Métamorphe และ Centaure มีความสามารถที่ AI ใช้ได้ไม่เต็มที่
  • Reinforcement learning: ให้มันเล่นกับตัวเองเพื่อปรับค่าสัมประสิทธิ์

แต่สำหรับเกมบนเบราว์เซอร์ก็ใช้ได้ เพื่อนบางคนยังแพ้มันอยู่ ก็ถือว่าใช้ได้ xD

ทดลองเล่น

เล่นได้ที่ nausicaa-game.github.io คลิก "JOUER" เปิด CPU mode แล้วดู AI เล่น

คำแนะนำ: ให้ AI เล่นกับตัวเอง คุณจะเห็นช่วงที่ aggressive แล้วจู่ ๆ ก็ถอยหมด

โค้ดอยู่บน GitHub ใน js/cpu.js

3 สิ่งที่จำไว้:

  1. ค่าสัมประสิทธิ์ฮิวริสติก -- ไม่มี minimax แต่ละยูนิตมีความน่าสนใจ
  2. ค่าสัมประสิทธิ์เปลี่ยนทุก 5 เทิร์น -- AI สลับระหว่าง aggressive และควบคุม แบบ Pac-Man
  3. Oracle หนี -- มันคำนวณทิศทางตรงข้ามภัยคุกคามและวิ่ง

ถ้าคุณมีไอเดียทำให้ AI โหดยิ่งขึ้น เปิด issue ได้เลย ฉันมีแผนสำหรับเวอร์ชันที่เรียนรู้จากความพ่ายแพ้ แต่คงไว้บทความหน้า xD

Related Articles