jumping back in

after several weeks of break time due to real life and several weeks of development at the usual slow pace, it is time for another blog post! it has been over 200 commits since the last post, so there certainly are a couple of things to write about..

funnily enough it seems like today is exactly 3 months after the last post. definitely a good time for an update post!

combat simulator

when i came back to working on arena the heading for all the upcoming work was “micro” (some of the other posts already included that). using more heuristics (because we can here, compared to world) to (hopefully) put up a better fight.

a big idea i had was working on simulating combat. the gist of it was that i write a simulator, throw terrain and some creeps (divided into two groups) at it and get a result back that tells me how well each side would be doing.

since i want to write about other things in this post i will shorten this part: i eventually scrapped the idea for now. i should have an easier time coming back to it now since i have been writing a lot of the new combat code as “agnostic” as possible, maybe i will come back to the simulator idea for a better look in the (potential) future. currently i just make combat decisions based on a single tick lookahead.

it really came down to how much refactoring would have been needed to not make such a core component a hacky mess.

improved targeting

while the full simulator was scrapped, i could reuse the targeting code i had been working on. i am basically doing what i was planning on doing with the simulator here, throwing a bunch of creeps at the targeting function and doing whatever it thinks is best. it currently does not take into account movement.

i calculate targeting in 3 steps:

  1. figure out what we want to hit for each creep
  2. guesstimate who we think they want to hit using the same method as in step 1 but reversing the input
  3. based on 1 and 2 figure out who to heal/pre-heal

the targeting is focussed on maximizing the damage output to where we think it will knock off the most bodyparts, while also trying to make sure we deal the most damage we can in a given tick (basically, use ranged mass attack if it deals more damage than a simple ranged attack).

right now i am considering an unrealistic “worst case scenario” to calculate the “effective” hp for creeps: for every of their creeps i calculate the maximum expected healing, ignoring that each creep can only heal one other creep. this probably results in fairly conservative decisions, but i think it is much better to work on this basis and get a nice surprise next tick (“wow, that hit for way more than expected”) rather than overestimating our potential damage output.

i was thinking about adding a fourth phase, where i would make adjustments based on the results so far, but i have not gotten around to really think about that in detail, so right now that is just a potential improvement to this system.

combat movement improvements

since i was testing combat a lot and was actually watching what was happening in detail i noticed a couple things that my combat movement did wrong. the two major improvements really helped with some very weird movement results that were happening in fights.

the first improvement was a fix for a functionality gap that existed purely due to laziness. it has not been hard to implement, i have just put it off for a long time: spicing my matrices up with proper handling for creep positions. i just never bothered doing this and had my matrix library optionally block off current creep positions. blocking current creep positions only makes sense for creeps with fatigue or stationary creeps though obviously. what we really care about are the expected next positions of our creeps, so that we can avoid those. that way we produce way less conflicts that get resolved by the engine later, and we end up with more control.

how does it work? at the start of the tick i go through an array of all creep objects that are alive. if a creep has fatigue, i record its current position as “taken”. i also monkey patched (again, laziness) move() to register the resulting next position as “taken”. that way i end up with a list of positions of all (mine and their) fatigued creeps and my next moves that i can flag with a higher cost in path finding.

the second improvement was a fix related to how path finding works: i give searchPath a bunch of goals, and it gives me back a path that satisfies the goal with the least cost.

my combat movement makes heavy use of this, and here is a (very oversimplified) description of what was happening because of that: if i want my creeps to stick together, a naive way to integrate that would be to pass them as goals with range 1. then as a next step you add the actual goal that the group should read. what happens? well, if all the creeps are in range 1 of each other, none move to the actual goal. why? because they all fulfill one of the “stick together” goals and searchPath returns the path for that.

something similar to that situation was sometimes happening to my creeps and i could never really figure out why, because it happened so sporadically. the fix was very simple when i finally figured it out though: i now filter the goals given to pathfinding. if a goal is reached, no need to searchPath anymore, right?

testing / mocks

with the (much appreciated) help of eduter i managed to get jest working with arena. this means i can now unit test code whereever necessary. i have used this for the targeting, not really writing complete unit tests, but just defining various cases where i know what i want to happen. this allows me to at least in some way verify functionality should i make changes to this core part of the bot(s). maybe i will extend the testing further later on.

what i have noticed however is that now vscode picks up on my “mocks” folder when suggesting imports. it is just a slight annoyance but yeah, i have to actively check imports when using “add all missing imports”, because the mock folder will slip in.

profiler

i wrote a basic profiler. i think it is pretty neat, so i will just share it here. 🙂


// constants
export const RATIO_NS_MS = 1000000;

// utilities
// myMaxBy is basically just lodashs _.max()

import { RATIO_NS_MS } from "common/utilities/constants/cpu";
import { PROFILER_ENABLED } from "common/utilities/constants/debug-flags";
import { myMaxBy } from "common/utilities/math";
import { getCpuTime } from "game/utils";

let stack: string[] = [];

interface ProfilerSection {
  begin: number;
  time: number;
}

let profile: {
  [name: string]: ProfilerSection;
} = {};

export class Profiler {
  public static init(): void {
    if (!PROFILER_ENABLED) {
      return;
    }
    stack = [];
    profile = {}
  }

  public static enter(name: string): void {
    if (!PROFILER_ENABLED) {
      return;
    }
    stack.push(name);
    const sectionName = stack.join(".");
    if (!profile[sectionName]) {
      profile[sectionName] = {
        begin: getCpuTime(),
        time: 0
      }
    }
  }

  public static leave(name: string): void {
    if (!PROFILER_ENABLED) {
      return;
    }
    do {
      if (stack.length === 0) {
        return;
      }
      const sectionName = stack.join(".");
      if (profile[sectionName]) {
        profile[sectionName].time = getCpuTime() - profile[sectionName].begin;
      }
      const latest = stack.pop();
      if (latest && latest === name) {
        return;
      }
      // eslint-disable-next-line no-constant-condition
    } while (true);
  }

  public static report(showTree = false): void {
    if (showTree) {
      const maxKeyLength = myMaxBy(Object.keys(profile), k => k.length)?.length ?? 0;
      for (const entry of Object.entries(profile)) {
        const cpu = (entry[1].time / RATIO_NS_MS).toFixed(2);
        console.log(`${entry[0].padEnd(maxKeyLength, " ")}: \x1b[38;5;220m${cpu.padStart(6, " ")}\x1b[0m`);
      }
      return;
    }
    console.log(`tick: \x1b[38;5;220m${(profile.tick.time / RATIO_NS_MS).toFixed(2).padStart(6, " ")}\x1b[0m`);
  }
}

// somewhere in my bot.tick() function
Profiler.enter("visuals");
this.visualize();
Profiler.leave("visuals");

console.log("stats");
Profiler.leave("tick");
console.log("profiler report");
Profiler.report(PROFILER_SHOW_TREE);

maybe it helps someone or inspires them to write their own.

strategic errors

playing a couple more basic spawn and swamp matches i noticed that i have to question some of the bots basic macro assumptions. right now i made a reactive or passive decision in pretty much any strategic question. while this does make sense in some regards (for example waiting a bit and giving time to the other player to spawn their first creep to gather some intel on what to build) it also really plays into the hands of the other player in a lot of others. what i have noticed happening in a lot of matches was that my bot had a military advantage that was easy to spot for a human, but since it would rather defend home base and the other bot was being aggressive, it just defended the whole game, turning and easily won game into a draw. or even worse, sometimes it would slip up in defense and let some creep through.

i will probably take a look at these decision making progresses next and sharpen up on some of the metrics, hopefully turning more of these matches into wins. 🙂

stats

  • code
    • last commit: d72d19e448281e5d8779f013af7d8e5aef02ba07
    • total commits: 938
    • total files: 146
    • total lines of code: 12694
  • ingame
    • basic ctf: rank #9, v30
    • basic sas: rank #49, v58