🤖 用 TypeScript 打造一個 Chrono Divide AI Bot:從部署到進攻的全自動流程解析

| Chrono Divide | 3 Reads

Chrono Divide 是一款基於 Red Alert 2 引擎重現的網頁即時戰略遊戲,提供了官方支援的 AI 開發 API。
本文將透過一份實戰程式碼,從零開始解析一個簡單的 AI Bot,教你如何讓 Bot 自動部署建造場、派兵出擊、發現敵人後主動攻擊!


🧱 前置準備

安裝套件

本教學使用 @chronodivide/game-api 套件來開發 AI Bot:

npm install @chronodivide/game-api

資源目錄

確保你已設定好 RA2 的資源目錄:

# Windows(例如 MIX 檔案放在 ./redalert2)
set MIX_DIR=./redalert2

📜 代碼總覽與註解說明

以下是完整的 Bot 原始碼,包含詳細中文註解,說明其如何管理狀態、行為決策與遊戲互動邏輯:

👇👇👇
請將以下原始碼替換為你整理過的那份註解代碼


// 匯入 Chrono Divide 官方提供的核心 API 類型與工具
import {
    cdapi,
    OrderType,
    ApiEventType,
    Bot,
    GameApi,
    ApiEvent,
    CreateBaseOpts,
    CreateOpts
} from "@chronodivide/game-api";

// 定義 Bot 的內部狀態,用於控制其行為流程
enum BotState {
    Initial,          // 初始狀態:等待或部署 MCV
    Deployed,         // 建造場已部署,開始集結部隊
    MovingToEnemy,    // 移動至敵方初始位置
    AttackingEnemy,   // 發現敵人建築,開始攻擊
    Defeated          // 無可操作單位,被判定戰敗
}

// 建立一個簡單的 Bot,繼承自官方 Bot 類別
class ExampleBot extends Bot {
    private botState = BotState.Initial;     // Bot 狀態初始為 Initial
    private tickRatio!: number;              // 控制 AI 執行頻率的 tick 比率
    private enemyPlayers!: string[];         // 紀錄敵方玩家名稱列表

    // 遊戲啟動時呼叫,取得 tickRate、敵人資訊並計算 bot 的 tickRatio
    override onGameStart(game: GameApi) {
        const gameRate = game.getTickRate();     // 每秒 tick 數
        const botApm = 300;                      // 假設 AI 有每分鐘 300 動作
        const botRate = botApm / 60;
        this.tickRatio = Math.ceil(gameRate / botRate);  // 決定多久 tick 執行一次

        // 找出非自己且非同盟的敵方玩家名稱
        this.enemyPlayers = game.getPlayers().filter(p =>
            p !== this.name && !game.areAlliedPlayers(this.name, p));
    }

    // 每個 tick 呼叫,根據 tickRatio 節流執行實際邏輯
    override onGameTick(game: GameApi) {
        if (game.getCurrentTick() % this.tickRatio === 0) {
            switch (this.botState) {
                case BotState.Initial: {
                    // 若已部署建造場則切換狀態
                    const baseUnits = game.getGeneralRules().baseUnit;
                    let conYards = game.getVisibleUnits(this.name, "self", r => r.constructionYard);
                    if (conYards.length) {
                        this.botState = BotState.Deployed;
                        break;
                    }
                    // 若還未部署建造場,嘗試對 MCV 使用 DeploySelected
                    const units = game.getVisibleUnits(this.name, "self", r => baseUnits.includes(r.name));
                    if (units.length) {
                        this.actionsApi.orderUnits([units[0]], OrderType.DeploySelected);
                    }
                    break;
                }

                case BotState.Deployed: {
                    // 部隊部署完成後,集合部隊準備前往敵方出生點
                    const armyUnits = game.getVisibleUnits(this.name, "self", r => r.isSelectableCombatant);
                    const { x: rx, y: ry } = game.getPlayerData(this.enemyPlayers[0]).startLocation;
                    this.actionsApi.orderUnits(armyUnits, OrderType.AttackMove, rx, ry);
                    this.botState = BotState.MovingToEnemy;
                    break;
                }

                case BotState.MovingToEnemy:
                case BotState.AttackingEnemy: {
                    // 每次判斷是否還有部隊可用
                    const armyUnits = game.getVisibleUnits(this.name, "self", r => r.isSelectableCombatant);
                    if (!armyUnits.length) {
                        // 若全軍覆沒則退出遊戲
                        this.botState = BotState.Defeated;
                        this.actionsApi.quitGame();
                        break;
                    }

                    if (this.botState === BotState.MovingToEnemy) {
                        // 若尚未偵測到敵方建築,持續前進;一旦看到建造場則開始進攻
                        const baseUnits = game.getGeneralRules().baseUnit;
                        const enemyBase = game.getVisibleUnits(this.name, "hostile",
                            r => r.constructionYard || baseUnits.includes(r.name));

                        if (enemyBase.length) {
                            // 指揮部隊攻擊發現的第一個敵方建築
                            this.actionsApi.orderUnits(armyUnits, OrderType.AttackMove, enemyBase[0]);
                            this.botState = BotState.AttackingEnemy;
                        }
                    }
                    break;
                }

                default:
                    break;
            }
        }
    }

    // 當遊戲中出現事件(例如單位毀滅、所有權轉移)時被觸發
    override onGameEvent(ev: ApiEvent) {
        switch (ev.type) {
            case ApiEventType.ObjectOwnerChange: {
                this.logger.info(`Owner change: ${ev.prevOwnerName} -> ${ev.newOwnerName}`);
                break;
            }

            case ApiEventType.ObjectDestroy: {
                this.logger.info(`Object destroyed: ${ev.target}`);
                break;
            }

            default:
                break;
        }
    }
}

// 遊戲主函數,負責初始化遊戲環境並啟動對戰
async function main() {
    // 初始化遊戲資源目錄 MIX_DIR
    await cdapi.init(process.env.MIX_DIR || "./");

    const mapName = "mp03t4.map"; // 使用指定的多人對戰地圖

    // 設定遊戲初始參數(適用於離線與線上模式)
    const baseOpts: CreateBaseOpts = {
        buildOffAlly: false,
        cratesAppear: false,
        credits: 10000,
        gameMode: cdapi.getAvailableGameModes(mapName)[0],
        gameSpeed: 5,
        mapName,
        mcvRepacks: true,
        shortGame: true,
        superWeapons: false,
        unitCount: 10
    };

    let opts: CreateOpts;

    // 判斷是否為線上模式(依據是否設定 SERVER_URL)
    const onlineMode = !!process.env.SERVER_URL;
    if (onlineMode) {
        // 從環境變數取得帳號設定
        const botName = process.env.BOT_USER;
        if (!botName) throw new Error(`Missing env BOT_USER`);

        const botPassword = process.env.BOT_PASS;
        if (!botPassword) throw new Error(`Missing env BOT_PASS`);

        const playerName = process.env.PLAYER_USER;
        if (!playerName) throw new Error(`Missing env PLAYER_USER`);

        // 線上模式:Bot vs 人類玩家
        opts = {
            ...baseOpts,
            online: true,
            serverUrl: process.env.SERVER_URL!,
            clientUrl: process.env.CLIENT_URL!,
            botPassword,
            agents: [
                new ExampleBot(botName, "Americans").setDebugMode(true),
                { name: playerName, country: "Africans" }
            ]
        };
    } else {
        // 離線模式:Bot vs Bot 測試
        opts = {
            ...baseOpts,
            agents: [
                new ExampleBot("Joe", "Americans").setDebugMode(true),
                new ExampleBot("Bob", "Africans")
            ]
        };
    }

    // 建立並啟動遊戲對戰
    const game = await cdapi.createGame(opts);

    // 主循環:直到遊戲結束
    while (!game.isFinished()) {
        await game.update();
    }

    // 儲存重播與清理資源
    game.saveReplay();
    game.dispose();
}

// 進入點,處理例外並啟動
main().catch(e => {
    console.error(e);
    process.exit(1);
});

🚦 執行方式

你可以透過兩種方式執行:

🔁 離線模式(Bot 對 Bot)

MIX_DIR=./redalert2 node bot.js
  • 自動讓 2 個 Bot 在地圖上對戰,方便測試戰術邏輯。

🌐 線上對戰模式(Bot 對人類)

SERVER_URL=wss://your-server \
BOT_USER=ai1 BOT_PASS=secret \
PLAYER_USER=humanplayer \
MIX_DIR=./redalert2 node bot.js
  • AI Bot 會透過 WebSocket 連線並與人類對手進行 PvP。


🧠 行為解析:這個 Bot 做了什麼?

階段 行為邏輯
🟢 初始(Initial) 檢查是否已有建造場;否則命令 MCV 部署
🏗️ 部署(Deployed) 建造場部署完成後,集結所有部隊,向敵人出生點出擊
🚶 移動(Moving) 沿路攻擊前進,如偵測到敵方建築則切換為進攻狀態
🔥 攻擊(Attacking) 鎖定第一個看到的敵方建築並持續進攻
💀 戰敗(Defeated) 無可用單位時自動投降並退出遊戲

🔍 可擴充方向

這只是個起點,你可以嘗試:

  • ➕ 加入建造邏輯(如電廠→兵營→坦克廠)

  • 💰 加入資源採集與礦車邏輯

  • 🎯 根據敵方部隊選擇不同戰術(反坦克、反步兵)

  • 🧠 加入地圖偵查、視野規劃與智能決策


📝 結語

這篇文章帶你從 0 開始建立一個基礎的 Chrono Divide AI Bot,理解狀態管理與 API 的互動方式。
透過不斷測試與優化,你可以打造出一個擁有自我決策能力的智慧對手!

如果你對 AI 對戰有興趣,這會是個絕佳的開發起點!
未來我們將深入探討 資源控制、建築排布、單位編隊、行為切換 AI 等進階主題,敬請期待 🚀

This article was last edited at