前言:
基于前面第三篇的介绍,这边来说下AI自动攻击引擎,这边和第二篇一样采用的基本的逻辑算法编写,没有额外采用网络上的游戏引擎.
代码如下:
TypeScript
import { Unit, Faction, Position } from '../common/GameModels';
import { GameEngine } from '../engine/GameEngine';
import { GamePhase } from '../common/GameConstants';
// AI引擎 - 控制敌方单位行动
export class AIEngine {
private gameEngine: GameEngine;
constructor(gameEngine: GameEngine) {
this.gameEngine = gameEngine;
}
// 执行AI回合
executeTurn(): Promise<void> {
return new Promise((resolve: () => void): void => {
const enemyUnits: Unit[] = this.gameEngine.getGameState().units
.filter((u: Unit): boolean => u.faction === Faction.ENEMY && u.isAlive && !u.hasActed);
if (enemyUnits.length === 0) {
resolve();
return;
}
// 逐个执行敌方单位行动
this.executeUnitActions(enemyUnits, 0, resolve);
});
}
// 逐个执行单位行动(带延迟以便观察)
private executeUnitActions(units: Unit[], index: number, resolve: () => void): void {
if (index >= units.length) {
resolve();
return;
}
const unit = units[index];
if (!unit.isAlive || unit.hasActed) {
this.executeUnitActions(units, index + 1, resolve);
return;
}
// 执行当前单位的行动
this.executeUnitAction(unit);
// 延迟后执行下一个单位
setTimeout((): void => {
this.executeUnitActions(units, index + 1, resolve);
}, 800);
}
// 执行单个单位的行动
private executeUnitAction(unit: Unit): void {
// 1. 尝试攻击(优先攻击)
const attackTarget = this.findBestAttackTarget(unit);
if (attackTarget) {
this.gameEngine.attackUnit(unit, attackTarget);
return;
}
// 2. 如果没有攻击目标,则移动
const moveTarget = this.findBestMoveTarget(unit);
if (moveTarget) {
this.gameEngine.moveUnit(unit, moveTarget);
// 移动后尝试攻击
const newAttackTarget = this.findBestAttackTarget(unit);
if (newAttackTarget) {
this.gameEngine.attackUnit(unit, newAttackTarget);
} else {
unit.hasActed = true;
}
} else {
// 无法移动,标记为已行动
unit.hasActed = true;
}
}
// 寻找最佳攻击目标
private findBestAttackTarget(unit: Unit): Position | null {
const attackablePositions: Position[] = this.gameEngine.getAttackablePositions(unit);
if (attackablePositions.length === 0) return null;
// 评估每个攻击目标的价值
let bestTarget: Position | null = null;
let bestValue = -1;
for (let i = 0; i < attackablePositions.length; i++) {
const pos = attackablePositions[i];
const targetUnit = this.gameEngine.getUnitAt(pos);
if (!targetUnit) continue;
// 计算目标价值(优先攻击血量低、攻击力高的单位)
const value = this.evaluateTargetValue(unit, targetUnit);
if (value > bestValue) {
bestValue = value;
bestTarget = pos;
}
}
return bestTarget;
}
// 评估攻击目标的价值
private evaluateTargetValue(attacker: Unit, target: Unit): number {
let value = 0;
// 1. 优先攻击将军(高价值目标)
if (target.type === 4) { // GENERAL
value += 100;
}
// 2. 优先攻击血量低的单位(可以击杀)
const hpPercent = target.hp / target.maxHp;
if (hpPercent < 0.3) {
value += 50; // 可能击杀
}
// 3. 考虑攻击力威胁
value += target.attack * 0.5;
// 4. 远程单位优先攻击近战单位
if (attacker.type === 1 || attacker.type === 3) { // ARCHER or MAGE
if (target.type === 0 || target.type === 2) { // WARRIOR or CAVALRY
value += 20;
}
}
// 5. 扣除距离惩罚(越近越好)
const distance = Math.abs(attacker.position.x - target.position.x) +
Math.abs(attacker.position.y - target.position.y);
value -= distance * 2;
return value;
}
// 寻找最佳移动目标
private findBestMoveTarget(unit: Unit): Position | null {
const movablePositions: Position[] = this.gameEngine.getMovablePositions(unit);
if (movablePositions.length === 0) return null;
// 找到最近的敌方单位
const enemyUnits: Unit[] = this.gameEngine.getGameState().units
.filter((u: Unit): boolean => u.faction === Faction.PLAYER && u.isAlive);
if (enemyUnits.length === 0) return null;
// 评估每个可移动位置的价值
let bestPosition: Position | null = null;
let bestValue = -1000;
for (let i = 0; i < movablePositions.length; i++) {
const pos = movablePositions[i];
const value = this.evaluateMovePosition(unit, pos, enemyUnits);
if (value > bestValue) {
bestValue = value;
bestPosition = pos;
}
}
return bestPosition;
}
// 评估移动位置的价值
private evaluateMovePosition(unit: Unit, pos: Position, enemyUnits: Unit[]): number {
let value = 0;
// 1. 接近敌方单位(移动范围允许的情况下)
let minDistanceToEnemy = 100;
for (let i = 0; i < enemyUnits.length; i++) {
const enemy = enemyUnits[i];
const distance = Math.abs(pos.x - enemy.position.x) +
Math.abs(pos.y - enemy.position.y);
minDistanceToEnemy = Math.min(minDistanceToEnemy, distance);
}
// 对于近战单位,想要接近敌人
if (unit.attackRange === 1) {
value += (10 - minDistanceToEnemy) * 10; // 越近越好
}
// 对于远程单位,保持距离但进入攻击范围
if (unit.attackRange > 1) {
if (minDistanceToEnemy <= unit.attackRange && minDistanceToEnemy > 1) {
value += 50; // 理想攻击位置
} else if (minDistanceToEnemy > unit.attackRange) {
value += (unit.attackRange - minDistanceToEnemy + 10) * 5; // 需要接近
}
}
// 2. 优先选择有利地形
const terrain = this.gameEngine.getTerrainAt(pos);
if (terrain === 1) { // FOREST
value += 10; // 防御加成
} else if (terrain === 2) { // MOUNTAIN
value += 15; // 更多防御加成
} else if (terrain === 3) { // WATER
value -= 100; // 避免水域
}
// 3. 避免过于冒险(低血量单位应该更保守)
const hpPercent = unit.hp / unit.maxHp;
if (hpPercent < 0.3) {
// 低血量,寻找防御位置
if (terrain === 1 || terrain === 2) {
value += 30;
}
// 远离敌方
value += minDistanceToEnemy * 3;
}
return value;
}
}
AIEngine 是回合制策略游戏中控制敌方单位自动行动的人工智能引擎,基于 TypeScript 实现。它依赖 GameEngine 提供的游戏状态与操作接口,通过启发式评估函数为每个敌方单位独立决策最优行动方案。AI 采用贪心策略,按"先攻击、后移动、再攻击"的优先级链执行,无需搜索树或蒙特卡洛模拟,属于轻量级规则驱动型 AI。
一:设计
- 架构层次: 位于 GameEngine 之上,消费其查询与操作接口,不直接修改 GameState
- 决策模型: 启发式评分 + 贪心选择,每个单位独立决策,无全局协同
- 执行方式: 异步串行,单位间 800ms 延迟,便于 UI 播放动画
- 扩展性: 评估函数可独立替换,支持从规则驱动升级为搜索驱动
依赖关系
- GameModels: Unit, Faction, Position 类型定义
- GameEngine: getGameState(), getAttackablePositions(), getMovablePositions(), attackUnit(), moveUnit(), getUnitAt(), getTerrainAt() 接口
- GameConstants: GamePhase 枚举(当前代码中未直接使用,但保留导入以备扩展)
二:类的接口介绍
AIEngine 是一个无状态决策器(除持有 gameEngine 引用外无内部状态),所有决策均基于当前 GameState 实时计算。其对外仅暴露 executeTurn() 一个方法,内部通过私有方法链完成完整的决策与执行流程。
AIEngine 方法总览
| 方法 | 可见性 | 类别 | 说明 |
|---|---|---|---|
| executeTurn() | public | 入口 | 执行整个 AI 回合,返回 Promise |
| executeUnitActions() | private | 调度 | 串行调度各单位行动,含 800ms 延迟 |
| executeUnitAction() | private | 决策 | 单个单位的行动决策与执行 |
| findBestAttackTarget() | private | 评估 | 从可攻击位置中选出最优攻击目标 |
| evaluateTargetValue() | private | 评估 | 计算攻击目标的价值分数 |
| findBestMoveTarget() | private | 评估 | 从可移动位置中选出最优移动目标 |
| evaluateMovePosition() | private | 评估 | 计算移动位置的价值分数 |
三:回合执行流程
3.1 executeTurn() 总控
executeTurn() 是 AI 引擎的唯一公共入口,返回 Promise<void>,使调用方可以 await 等待整个 AI 回合执行完毕。方法首先筛选所有存活且未行动的敌方单位,若无可用单位则立即 resolve,否则将单位列表传入递归调度器。
3.1.1 执行时序
- 从 GameState 中筛选 faction=ENEMY 且 isAlive=true 且 hasActed=false 的单位列表
- 若列表为空,立即 resolve Promise
- 调用 executeUnitActions(units, 0, resolve) 开始串行调度
- 所有单位行动完成后,resolve Promise,控制权交还 GameEngine
3.2 executeUnitActions() 串行调度
该方法通过递归 + setTimeout 实现单位间的串行执行与视觉延迟。每个单位执行完毕后等待 800ms 再处理下一个,便于 UI 层播放移动和攻击动画。
调度逻辑分支
| 条件 | 处理 | 说明 |
|---|---|---|
| index >= units.length | 调用 resolve() | 所有单位已处理完毕 |
| unit 已死亡或已行动 | 跳过,处理下一个 | 战斗中可能被反击击杀 |
| unit 存活且未行动 | 执行行动,800ms 后处理下一个 | 正常行动流程 |
四:单位行动决策
executeUnitAction() 方法为每个单位按固定优先级链执行决策,遵循"能打就打,不能打就移动,移动后再打"的贪心策略。
- 调用 findBestAttackTarget() 检查当前位置是否有可攻击目标
- 若存在最优攻击目标,立即执行攻击并结束本回合
- 若无攻击目标,调用 findBestMoveTarget() 寻找最优移动位置
- 移动后再次调用 findBestAttackTarget() 检查新位置是否可攻击
- 若移动后可攻击,执行攻击;否则手动标记 hasActed=true 结束回合
- 若无法移动,直接标记 hasActed=true 结束回合
五:攻击目标评估
5.1 findBestAttackTarget() 选择逻辑
该方法遍历当前单位的所有可攻击位置,对每个位置上的敌方单位调用 evaluateTargetValue() 计算价值分数,选取得分最高的目标返回。若无有效目标则返回 null。
5.2 evaluateTargetValue() 评分体系
攻击目标评估函数是一个加权求和模型,包含五个独立评估维度。每个维度贡献一个分值,最终分数越高表示目标越值得攻击。
攻击目标评估维度
| 维度 | 条件 | 分值 | 设计意图 |
|---|---|---|---|
| 将军优先 | target.type === 4 (GENERAL) | +100 | 将军是核心单位,优先消灭可致胜 |
| 残血击杀 | hp/maxHp < 0.3 | +50 | 低血量单位更可能被击杀,减少敌方行动次数 |
| 威胁评估 | 始终 | +target.attack * 0.5 | 高攻击力单位威胁更大,优先消除 |
| 兵种克制 | 远程打近战 | +20 | 利用射程优势,远程先消耗近战 |
| 距离惩罚 | 始终 | -distance * 2 | 距离越远越不利于后续追击 |
评分示例:
假设一名法师(type=3, 远程)评估两个目标:近战战士(attack=8, hp=30%, 距离2)和将军(attack=5, hp=80%, 距离3)。
评分对比示例
| 评估维度 | 战士得分 | 将军得分 |
|---|---|---|
| 将军优先 | 0 | +100 |
| 残血击杀 | +50 | 0 |
| 威胁评估 | +4 (8*0.5) | +2.5 (5*0.5) |
| 兵种克制 | +20 | +20 |
| 距离惩罚 | -4 (2*2) | -6 (3*2) |
| 合计 | 70 | 116.5 |
结果将军得分(116.5)高于战士(70),AI 会优先攻击将军。这符合"优先消灭核心目标"的设计意图,但也暴露了将军优先权重过高可能导致忽略击杀机会的问题。
六:移动目标评估
6.1 findBestMoveTarget() 选择逻辑
该方法遍历当前单位的所有可移动位置,对每个位置调用 evaluateMovePosition() 计算价值分数,选取得分最高的位置返回。评估时需传入所有存活玩家单位列表,用于计算距离关系。
6.2 evaluateMovePosition() 评分体系
移动位置评估函数同样采用加权求和模型,从兵种定位、地形优势和生存策略三个维度综合评估。
6.2.1 近战单位定位
近战单位(attackRange === 1)的移动目标是接近敌人,评分公式为:(10 - minDistanceToEnemy) * 10。距离越近得分越高,最大加分 100(贴脸),最小 0(距离10+)。这驱动近战单位始终向最近的敌人冲锋。
6.2.2 远程单位定位
远程单位(attackRange > 1)追求"在攻击范围内但保持安全距离"的理想位置:
远程单位移动评分规则
| 距离条件 | 评分 | 含义 |
|---|---|---|
| 1 < 距离 <= attackRange | +50 | 理想攻击位置,可攻击且安全 |
| 距离 > attackRange | +(attackRange - 距离 + 10) * 5 | 需接近,但仍向有利方向移动 |
| 距离 = 1 | 无额外加分 | 贴脸危险,远程不应近身 |
远程单位在距离=1时不会获得理想位置加分,但也不会主动远离。这是一个潜在的改进点:远程单位应主动回避近身接触。
6.2.3 地形偏好
地形评分规则
| 地形 | 类型 | 评分 | 原因 |
|---|---|---|---|
| 0 | 平原 | 0 | 基准,无特殊效果 |
| 1 | 森林 | +10 | 提供防御加成 |
| 2 | 山地 | +15 | 更高防御加成 |
| 3 | 水域 | -100 | 不可通行,强烈回避 |
6.2.4 低血量生存策略
当单位血量低于 30% 时,触发保守策略:森林或山地额外 +30 分(寻找掩体),同时 minDistanceToEnemy * 3 作为远离敌人的加分。这使得低血量单位倾向于退入有利地形并拉开距离,而非继续进攻。
低血量策略对比
| 状态 | 近战行为 | 远程行为 | 地形偏好 |
|---|---|---|---|
| 正常血量 | 冲向最近敌人 | 进入攻击范围 | 轻微偏好防御地形 |
| 低血量(<30%) | 仍冲向敌人* | 仍进入攻击范围* | 强烈偏好防御地形+远离敌人 |
七、与 GameEngine 的交互
7.1 调用的 GameEngine 接口
AIEngine 对 GameEngine 的接口依赖
| 接口 | 用途 | 调用场景 |
|---|---|---|
| getGameState() | 获取全局状态和单位列表 | executeTurn(), findBestMoveTarget() |
| getAttackablePositions() | 获取可攻击位置列表 | findBestAttackTarget() |
| getMovablePositions() | 获取可移动位置列表 | findBestMoveTarget() |
| getUnitAt() | 获取指定位置的单位 | findBestAttackTarget() |
| getTerrainAt() | 获取指定位置的地形 | evaluateMovePosition() |
| attackUnit() | 执行攻击操作 | executeUnitAction() |
| moveUnit() | 执行移动操作 | executeUnitAction() |
总结:
- 异步串行执行 + 延迟动画,用户体验友好,可观察每个 AI 单位的行动过程
- 攻击-移动-攻击的三段式决策链,模拟真实战术中的"移动接敌→攻击"行为
- 评估函数多维度加权,涵盖兵种克制、地形利用、生存策略等战术要素
- 低血量保守策略体现了生存意识,避免单位无意义送死
- 与 GameEngine 解耦,仅通过公共接口交互,便于替换 AI 策略