From e08e1840f2761a317f602005b30a005e2e054b5d Mon Sep 17 00:00:00 2001 From: dharryc Date: Mon, 12 Oct 2026 21:46:44 -0600 Subject: [PATCH] bots no longer target first oponent --- docs/tutorial/06-attack-and-win.md | 31 +++++++++++++++--------------- docs/tutorial/code/step4_attack.cs | 18 ++++++++--------- docs/tutorial/code/step4_attack.py | 13 +++++++------ samples/csharp/StarterBot.cs | 12 ++++++++++-- samples/python/bot.py | 12 ++++++------ src/NetRts.Bots/BotView.cs | 11 ++++++----- 6 files changed, 54 insertions(+), 43 deletions(-) diff --git a/docs/tutorial/06-attack-and-win.md b/docs/tutorial/06-attack-and-win.md index e7198d8..b8c9dd7 100644 --- a/docs/tutorial/06-attack-and-win.md +++ b/docs/tutorial/06-attack-and-win.md @@ -18,26 +18,27 @@ You can't see the enemy base through the fog, but you don't need to. Every playe `startPosition` is in the state, and that's where their Command Center was built: ```csharp -// Start position of the first opponent still in the game: its Command Center is there. -(int X, int Y) EnemyBase(GameState state) +// Start position of the nearest opponent still in the game: its Command Center is there. +(int X, int Y) EnemyBase(GameState state, int fromX, int fromY) { int me = state.You.Slot; - foreach (var p in state.Players) - { - if (p.Slot != me && !p.Eliminated) - return (p.StartPosition.X, p.StartPosition.Y); - } - return (state.MapWidth / 2, state.MapHeight / 2); + var rival = state.Players + .Where(p => p.Slot != me && !p.Eliminated) + .MinBy(p => Distance(fromX, fromY, p.StartPosition.X, p.StartPosition.Y)); + return rival is null + ? (state.MapWidth / 2, state.MapHeight / 2) + : (rival.StartPosition.X, rival.StartPosition.Y); } ``` ```python -def enemy_base(state): - """Start position of the first opponent still in the game: its Command Center is there.""" +def enemy_base(state, x, y): + """Start position of the nearest opponent still in the game: its Command Center is there.""" me = state["you"]["slot"] - for p in state["players"]: - if p["slot"] != me and not p["eliminated"]: - return p["startPosition"]["x"], p["startPosition"]["y"] + rivals = [p for p in state["players"] if p["slot"] != me and not p["eliminated"]] + if rivals: + p = min(rivals, key=lambda p: distance(x, y, p["startPosition"]["x"], p["startPosition"]["y"])) + return p["startPosition"]["x"], p["startPosition"]["y"] return state["mapWidth"] // 2, state["mapHeight"] // 2 ``` @@ -74,7 +75,7 @@ Two kinds of `Attack` order are useful here: else { // Attack-move: walk to the enemy base, fighting anything seen on the way. - var (x, y) = EnemyBase(state); + var (x, y) = EnemyBase(state, idleSoldiers[0].X, idleSoldiers[0].Y); commands.Add(new Command("Attack", UnitIds: ids, X: x, Y: y)); } } @@ -97,7 +98,7 @@ Two kinds of `Attack` order are useful here: commands.append({"type": "Attack", "unitIds": ids, "targetId": target["id"]}) else: # Attack-move: walk to the enemy base, fighting anything seen on the way. - x, y = enemy_base(state) + x, y = enemy_base(state, idle_soldiers[0]["x"], idle_soldiers[0]["y"]) commands.append({"type": "Attack", "unitIds": ids, "x": x, "y": y}) ``` diff --git a/docs/tutorial/code/step4_attack.cs b/docs/tutorial/code/step4_attack.cs index 5adf32e..7bae97c 100644 --- a/docs/tutorial/code/step4_attack.cs +++ b/docs/tutorial/code/step4_attack.cs @@ -148,16 +148,16 @@ async Task WaitForStart(string matchId) return best; } -// Start position of the first opponent still in the game: its Command Center is there. -(int X, int Y) EnemyBase(GameState state) +// Start position of the nearest opponent still in the game: its Command Center is there. +(int X, int Y) EnemyBase(GameState state, int fromX, int fromY) { int me = state.You.Slot; - foreach (var p in state.Players) - { - if (p.Slot != me && !p.Eliminated) - return (p.StartPosition.X, p.StartPosition.Y); - } - return (state.MapWidth / 2, state.MapHeight / 2); + var rival = state.Players + .Where(p => p.Slot != me && !p.Eliminated) + .MinBy(p => Distance(fromX, fromY, p.StartPosition.X, p.StartPosition.Y)); + return rival is null + ? (state.MapWidth / 2, state.MapHeight / 2) + : (rival.StartPosition.X, rival.StartPosition.Y); } // Look at this tick's state and return a list of commands to send. @@ -255,7 +255,7 @@ List Decide(GameState state) else { // Attack-move: walk to the enemy base, fighting anything seen on the way. - var (x, y) = EnemyBase(state); + var (x, y) = EnemyBase(state, idleSoldiers[0].X, idleSoldiers[0].Y); commands.Add(new Command("Attack", UnitIds: ids, X: x, Y: y)); } } diff --git a/docs/tutorial/code/step4_attack.py b/docs/tutorial/code/step4_attack.py index bfe6ecf..a44d363 100644 --- a/docs/tutorial/code/step4_attack.py +++ b/docs/tutorial/code/step4_attack.py @@ -130,12 +130,13 @@ def find_build_site(command_center, state): return best -def enemy_base(state): - """Start position of the first opponent still in the game: its Command Center is there.""" +def enemy_base(state, x, y): + """Start position of the nearest opponent still in the game: its Command Center is there.""" me = state["you"]["slot"] - for p in state["players"]: - if p["slot"] != me and not p["eliminated"]: - return p["startPosition"]["x"], p["startPosition"]["y"] + rivals = [p for p in state["players"] if p["slot"] != me and not p["eliminated"]] + if rivals: + p = min(rivals, key=lambda p: distance(x, y, p["startPosition"]["x"], p["startPosition"]["y"])) + return p["startPosition"]["x"], p["startPosition"]["y"] return state["mapWidth"] // 2, state["mapHeight"] // 2 @@ -213,7 +214,7 @@ def decide(state): commands.append({"type": "Attack", "unitIds": ids, "targetId": target["id"]}) else: # Attack-move: walk to the enemy base, fighting anything seen on the way. - x, y = enemy_base(state) + x, y = enemy_base(state, idle_soldiers[0]["x"], idle_soldiers[0]["y"]) commands.append({"type": "Attack", "unitIds": ids, "x": x, "y": y}) return commands diff --git a/samples/csharp/StarterBot.cs b/samples/csharp/StarterBot.cs index b8dd8ea..989a3c8 100644 --- a/samples/csharp/StarterBot.cs +++ b/samples/csharp/StarterBot.cs @@ -164,8 +164,16 @@ object TrainSoldier(State s, int ore) // 5. Once we have 5 soldiers, send every idle soldier to attack the enemy's starting corner. object Attack(State s) { - var enemy = s.Players.FirstOrDefault(p => p.Slot != s.You.Slot && !p.Eliminated); - if (MyUnits(s, "Soldier").Count < 5 || enemy == null) + var soldiers = MyUnits(s, "Soldier"); + if (soldiers.Count < 5) + { + return null; + } + + var enemy = s.Players + .Where(p => p.Slot != s.You.Slot && !p.Eliminated) + .MinBy(p => Math.Max(Math.Abs(p.StartPosition.X - soldiers[0].X), Math.Abs(p.StartPosition.Y - soldiers[0].Y))); + if (enemy == null) { return null; } diff --git a/samples/python/bot.py b/samples/python/bot.py index a689cca..b8cb0fe 100644 --- a/samples/python/bot.py +++ b/samples/python/bot.py @@ -137,16 +137,16 @@ def spend(kind): target = min(enemies, key=lambda e: dist(sx, sy, e["x"], e["y"])) commands.append({"type": "Attack", "unitIds": idle, "targetId": target["id"]}) else: # attack-move toward the enemy base (engages anything seen on the way) - tx, ty = self.enemy_base(s) + tx, ty = self.enemy_base(s, soldiers[0]["x"], soldiers[0]["y"]) commands.append({"type": "Attack", "unitIds": idle, "x": tx, "y": ty}) return commands - def enemy_base(self, s): - """Start position of the first opponent still in the game.""" + def enemy_base(self, s, x, y): me = s["you"]["slot"] - for p in s["players"]: - if p["slot"] != me and not p["eliminated"]: - return p["startPosition"]["x"], p["startPosition"]["y"] + rivals = [p for p in s["players"] if p["slot"] != me and not p["eliminated"]] + if rivals: + p = min(rivals, key=lambda p: dist(x, y, p["startPosition"]["x"], p["startPosition"]["y"])) + return p["startPosition"]["x"], p["startPosition"]["y"] return s["mapWidth"] // 2, s["mapHeight"] // 2 def find_site(self, cc, s): diff --git a/src/NetRts.Bots/BotView.cs b/src/NetRts.Bots/BotView.cs index 1eb8791..0f7ae56 100644 --- a/src/NetRts.Bots/BotView.cs +++ b/src/NetRts.Bots/BotView.cs @@ -45,9 +45,10 @@ public BotView(BotContext context) public bool IsOpen(int x, int y) => x >= 0 && y >= 0 && x < Map.Width && y < Map.Height && Map.Terrain[y][x] == '.' && !_blocked.Contains((x, y)); - /// Where the opposing bases started; the first one still in the game. - public PositionDto? EnemyStart() => - State.Players.Where(p => p.Slot != State.You.Slot && !p.Eliminated).Select(p => p.StartPosition).FirstOrDefault(); + public PositionDto? EnemyStart(int fromX, int fromY) => + State.Players.Where(p => p.Slot != State.You.Slot && !p.Eliminated) + .OrderBy(p => PlanBot.Dist(p.StartPosition.X, p.StartPosition.Y, fromX, fromY)).ThenBy(p => p.Slot) + .Select(p => p.StartPosition).FirstOrDefault(); /// Nearest visible enemy building, else the enemy's starting base. public (int X, int Y)? AttackTarget() @@ -59,7 +60,7 @@ public bool IsOpen(int x, int y) => return (building.X, building.Y); } - return EnemyStart() is { } start ? (start.X, start.Y) : null; + return EnemyStart(cc.X, cc.Y) is { } start ? (start.X, start.Y) : null; } /// Where the army waits: a few tiles from the Command Center toward the map centre. @@ -141,7 +142,7 @@ public bool IsOpen(int x, int y) => : ((int)Math.Round(ore.Average(r => r.X)), (int)Math.Round(ore.Average(r => r.Y))); } - case SitePreference.TowardEnemy when EnemyStart() is { } enemy: + case SitePreference.TowardEnemy when EnemyStart(cc.X, cc.Y) is { } enemy: { var dx = Math.Sign(enemy.X - cc.X); var dy = Math.Sign(enemy.Y - cc.Y);