From ad38d51c295a8b1b9f95bf1d6f42994de3696f76 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 09:08:52 +0000 Subject: [PATCH] fix(example): stop Commandery RTS opening deathball MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit choose() is deterministic, so a 0.60 assault score beat capture every first tick and walked Frontier across the map. Gate assault on force parity, garrison the east mine during raid, and picket Crown on the bridge so the opening is an economic contest. Co-authored-by: 西风逍遥游 --- examples/commandery-rts/README.md | 9 ++++--- examples/commandery-rts/main.nut | 41 ++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/examples/commandery-rts/README.md b/examples/commandery-rts/README.md index af4b47d8..ee7a6dd7 100644 --- a/examples/commandery-rts/README.md +++ b/examples/commandery-rts/README.md @@ -41,10 +41,11 @@ Controls: - `R`: reset the scenario. - The right-hand panel mirrors those buttons. -The opening deploys Crown around the north mine and Frontier around the east mine -so the first minute is an economic contest. Frontier AI starts in `raid` (contest -mines, train replacements) and only flips to `assault` from live force/mine -counts. The match ends when one side has no living units. +The opening deploys Crown on the north and bridge mines, Frontier on the east +mine. Frontier AI starts in `raid`: hold while outnumbered, leave a garrison, +contest the nearest Crown mine, and train up. It only flips to `assault` once it +has at least three units and is not outnumbered. The match ends when one side +has no living units. The engine module does not contain `General`, `Rebellion`, or a fixed RTS AI type; those rules are all in `main.nut`. diff --git a/examples/commandery-rts/main.nut b/examples/commandery-rts/main.nut index 5d3a7cad..5fe2e62b 100644 --- a/examples/commandery-rts/main.nut +++ b/examples/commandery-rts/main.nut @@ -115,13 +115,13 @@ function resetGame() { "frontier assault->raid"); requireResult(game.mind.newGrid("threat", 10, 6, 100.0, 0.0, 80.0), "threat grid"); - // Crown starts near the north mine; Frontier garrisons the east mine so the - // opening is an economic contest instead of an instant deathball. - game.units.push(makeUnit("crown.tank.1", game.crown, "tank", 220.0, 250.0, game.general)); - game.units.push(makeUnit("crown.infantry.1", game.crown, "infantry", 280.0, 230.0, game.general)); - game.units.push(makeUnit("crown.infantry.2", game.crown, "infantry", 175.0, 520.0, game.general)); + // Crown holds north + a picket on the bridge mine; Frontier garrisons east. + // The opening is an economic contest, not a map-wide deathball. + game.units.push(makeUnit("crown.tank.1", game.crown, "tank", 240.0, 240.0, game.general)); + game.units.push(makeUnit("crown.infantry.1", game.crown, "infantry", 290.0, 200.0, game.general)); + game.units.push(makeUnit("crown.infantry.2", game.crown, "infantry", 500.0, 360.0, game.general)); game.units.push(makeUnit("frontier.tank.1", game.frontier, "tank", 800.0, 250.0, "general.boros")); - game.units.push(makeUnit("frontier.infantry.1", game.frontier, "infantry", 760.0, 220.0, "general.boros")); + game.units.push(makeUnit("frontier.infantry.1", game.frontier, "infantry", 740.0, 210.0, "general.boros")); refreshAdministration(); refreshPanel(); } @@ -440,25 +440,38 @@ function updateEnemyAI(dt) { local crownN = livingCount(game.crown); local frontierN = livingCount(game.frontier); local crownMines=0; foreach(p in game.points) if(p.owner==game.crown) crownMines+=1; - local assaultUrge = crownN > 0 ? clamp(0.35 + (3-frontierN)*0.15 + (3-crownMines)*0.1, 0.05, 0.95) : 0.05; - local captureUrge = crownMines > 0 ? 0.55 : 0.2; - local holdUrge = frontierN <= 1 ? 0.7 : 0.25; - local action=game.mind.choose("assault="+assaultUrge+":2;capture="+captureUrge+":2;hold="+holdUrge+":1"); - if(action=="assault" && !game.rebelled) { + local forceDelta = frontierN - crownN; + // Assault is gated: outnumbered openings always held/raided. choose() is + // deterministic, so a 0.60 assault score previously won every first tick. + local assaultUrge = 0.05; + if (crownN > 0 && frontierN >= 3 && forceDelta >= 0) { + assaultUrge = clamp(0.40 + forceDelta * 0.18, 0.40, 0.90); + } + local captureUrge = (crownMines > 0 && frontierN >= 2) ? 0.70 : 0.15; + local holdUrge = forceDelta < 0 ? 0.78 : 0.28; + local action=game.mind.choose("assault="+assaultUrge+":1;capture="+captureUrge+":1;hold="+holdUrge+":1"); + if(action=="assault" && !game.rebelled && frontierN >= 3 && forceDelta >= 0) { local triggerResult = game.mind.trigger("frontier.ai", "base_exposed"); if (!triggerResult.ok) { game.message = triggerResult.status.summary; return; } - } else if (action=="hold" || action=="capture") { + } else { local holdResult = game.mind.trigger("frontier.ai", "hold_mines"); if (!holdResult.ok) { game.message = holdResult.status.summary; return; } } local aiState = game.mind.state("frontier.ai"); local captureTarget = nearestPoint(game.frontier, true); + local ownedMine = nearestPoint(game.frontier, false); + local assigned = 0; foreach (u in game.units) if(u.alive && u.faction==game.frontier) { if (aiState=="assault") { local enemy=nearestEnemy(u); if(enemy!=null){u.tx=enemy.x;u.ty=enemy.y;} - } else if (captureTarget != null) { - u.tx = captureTarget.x; u.ty = captureTarget.y; + } else { + assigned += 1; + if (action == "hold" || (assigned == 1 && ownedMine != null)) { + if (ownedMine != null) { u.tx = ownedMine.x; u.ty = ownedMine.y; } + } else if (captureTarget != null) { + u.tx = captureTarget.x; u.ty = captureTarget.y; + } } } if (frontierN < 3) queueEnemyUnit();