Tactical Combat AI Mod

A place for discussion of making game modifications.
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

Okay, decoupling ship movement order from shooting order did add a bit of complexity, but it's finally done, and along the way I cleaned up a lot of stuff, and found and fixed a few other issues. These fixes and improvements are also included in the non-decoupled-movement version (yay git).

It does make control flow for the ship loop that runs during the AI's turn a fair bit more complicated, since instead of just looping through all the ships we have to manage a pair of queues and jump back and forth between them as necessary. That said, it can make the AI's turn more satisfying to watch, since the game appears to merge consecutive movements and attacks together. This also makes the AI's turn pass a lot quicker than when it alternated between moving and shooting for every ship.
Attachments
CombatAIMod v1.2indev-decoupled-movement (6d750b1).zip
(22.19 KiB) Downloaded 590 times
CombatAIMod v1.2indev (2d497fc).zip
(21.24 KiB) Downloaded 588 times
zolobolo
Posts: 1544
Joined: Fri Nov 25, 2016 3:49 pm

Re: Tactical Combat AI Mod

Post by zolobolo »

Movement of AI is indeed much improved to look at and speeds up testing as well: great addition!

Missile focused ships do not move into firing range since decoupling exposing their missiles to counter fire and not just reaction PD - Save Mod Testing 6 (let me know if you haven't got the save for the test scenario)

Below error pops in following save:
https://www.dropbox.com/s/g6iudf9oxylls ... 2.rar?dl=0

The error might be related to using Assault Shuttles as I haven't seen those used since the latest update and the error comes around when such a vessel is in action
Attachments
Error.png
Error.png (148.32 KiB) Viewed 18267 times
zolobolo
Posts: 1544
Joined: Fri Nov 25, 2016 3:49 pm

Re: Tactical Combat AI Mod

Post by zolobolo »

Fleet vs Carrier + Planet Defense ends in the fleet loosing as the ships move directly against the carrier instead of prioritizing the bombers sortied out (probably the firing arc issue again: moving before selecting target to move against)

Save: Mod Testing 4
Attachments
Fleet vs Carrier.png
Fleet vs Carrier.png (501.41 KiB) Viewed 18265 times
zolobolo
Posts: 1544
Joined: Fri Nov 25, 2016 3:49 pm

Re: Tactical Combat AI Mod

Post by zolobolo »

There seems to be cases where the game hangs during AI processing turns
No errors in game or in Windows Event Log, just the AI Turn not ending

Trying to reproduce
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

zolobolo wrote:Below error pops in following save:
https://www.dropbox.com/s/g6iudf9oxylls ... 2.rar?dl=0

The error might be related to using Assault Shuttles as I haven't seen those used since the latest update and the error comes around when such a vessel is in action
Indeed it was related to assault shuttles. Fixed now.
zolobolo wrote:Fleet vs Carrier + Planet Defense ends in the fleet loosing as the ships move directly against the carrier instead of prioritizing the bombers sortied out (probably the firing arc issue again: moving before selecting target to move against)
Strange, I didn't see this when running the test case. The carrier simply did not launch fighters until in close combat (as intended). A few of the fighters were killed by reaction PD, most of them were actually killed in their hangars due to raiding(!), but the carrier never gave the Teros ships the opportunity to focus fire on the strike fighters. Eventually the Teros win as the first few turns of free attacks (fleet carriers are pretty slow, and took some time to get into close combat) allowed them to whittle down the shields and deal damage, forcing the fleet carrier to retreat. The remaining craft then defeat the planet (although in one out of the eight or so times I ran it, the carrier destroyed enough ships to enable the planet to win. I guess the raids got unlucky and hit bulkheads or something)

It appears to be a secondary effect from the pairs error. When I undo my fix the Teros lose because the transport is bugged and doesn't do anything. As I described above that transport is instrumental in their ability to defeat the carrier. So yeah, when an error like that occurs during a battle it can of course affect other things as the execution of the current function is halted.
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

zolobolo wrote:Missile focused ships do not move into firing range since decoupling exposing their missiles to counter fire and not just reaction PD - Save Mod Testing 6 (let me know if you haven't got the save for the test scenario)
They almost get there though... I'm pretty sure this is due the fact that missile ranges are handled differently from direct fire ranges. I've reduced the missile desired engagement range to compensate for this more. It's crude, and sometimes the ships still choose targets that are just a little bit out of range, but I think it helps.

EDIT: also just found and fixed a bug related to this, that was causing the AI to use beam weapon distance-to-target instead of launcher distance-to-target when adjusting target priority due to range. Now the only time I see missile carrying ships failing to close is when they are distracted by a nearby tracker.

When the AI considers the opposing point defence to be weak enough that its missiles can get through, it will still try to hang further back.
zolobolo wrote:There seems to be cases where the game hangs during AI processing turns
No errors in game or in Windows Event Log, just the AI Turn not ending

Trying to reproduce
That is very concerning. I haven't experienced it myself, but I've added some checks to hopefully try to turn it into an error that can be traced.
Last edited by harpy eagle on Fri Apr 13, 2018 5:11 pm, edited 1 time in total.
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

Just ran into the hang bug :(

Even with the added checks, the hang wasn't converted into an error. Really scratching my head over this. I've put checks that call error() everywhere that could possibly cause an infinite loop.

Here is the relevant code. None of the functions called contain loops that could cause the hang.

Code: Select all

-- So decoupling AI ship movement from AI ship attacks has complicated the main turn ship loop a bit
-- Instead of just iterating through all the ships in turn order, we have two separate queues.
-- Each iteration we pop a ship from the move queue, move it, then see if we can make any attacks
-- with the next ship in the attack queue.
--
-- Ships only attack after their movement has been handled, in order to give them an opportunity to
-- move within range of possible targets.
--
-- If we can attack with the next ship in the attack queue, we keep going through the attack queue until we
-- get a ship that hasn't moved yet. Then we go back into the move queue.
--
-- If a ship's move target gets destroyed, we put it back in both queues and return to handling movement.
-- This gives the ship the opportunity to move to a new target if it has movement points left
-- (and replicates the behaviour of the old close_and_attack() routine's while loop).
function CombatAI_Method:issue_ship_orders(general_retreat)
  local has_moved = {}
  local move_targets = {}

  local move_queue = sort_move_order(current_sides_ships, self)
  local attack_queue = sort_attack_order(current_sides_ships, self)

  local loop_sanity = 0
  while #move_queue > 0 and not battle_is_over() do
    loop_sanity++
    if loop_sanity > 1000 error("loop_sanity") end

    -- move a ship
    local move_ship = pop_back(move_queue)

    -- if we are going to retreat this ship, we have to decide that now
    if self:wants_to_retreat(move_ship, general_retreat)
      self:do_retreat(move_ship)
    else
      local attack_dist = desired_attack_dist(move_ship, self.estimated_tracker_survival)
      if attack_dist
        local move_target = self:choose_move_target(move_ship, attack_dist) or closest_enemy_ship(object_center(move_ship))
        if move_target
          if invalid_move_target(move_ship, move_target)
            print(move_ship.name, move_ship.empire.name, move_target.name)
            error("chose a move target that was invalid")
          end
          move_targets[move_ship] = move_target
          self:close_with_target(move_ship, move_target, attack_dist)
        end
      end
    end

    has_moved[move_ship] = true

    -- perform attacks, if we can
    local attack_ship = attack_queue[#attack_queue]

    local inner_loop_sanity = 0
    while #attack_queue > 0 and has_moved[attack_ship] and not battle_is_over() do
      inner_loop_sanity++
      if inner_loop_sanity > 1000 error("inner_loop_sanity") end

      local move_target = move_targets[attack_ship]

      if move_target
        if move_target.dead or move_target.defenseless or not enemies(attack_ship,move_target)
          has_moved[attack_ship] = nil
          move_queue[#move_queue+1] = attack_ship
          goto continue --go back to ship movement
        else
          self:engage_targets(attack_ship, move_target)

          --check again after attacking
          if move_target.dead or move_target.defenseless or not enemies(attack_ship,move_target)
            has_moved[attack_ship] = nil
            move_queue[#move_queue+1] = attack_ship
            goto continue --go back to ship movement
          end
        end
      else
        --if cloaked, don't attack if we are retreating (since this will decloak us)
        if not (attack_ship.cloaked and attack_ship.retreating)
          self:fire_at_will(attack_ship)
        end
      end

      --advance the attack queue
      pop_back(attack_queue)
      attack_ship = attack_queue[#attack_queue]
    end

    ::continue::
  end

  assert(battle_is_over() or (#move_queue == 0 and #attack_queue == 0))
end
zolobolo
Posts: 1544
Joined: Fri Nov 25, 2016 3:49 pm

Re: Tactical Combat AI Mod

Post by zolobolo »

Sorry for the radio silence I was trying to reproduce the hang bug but the little one got sick so it was a long day

My impression thus far is that it has more to do with the strategic AI as in one instance the game didn't hang at the same point and I haven't seen a battle that has occurred: now it could have been the absence of the specific battle the second time around that has lead to the evasion of the hang, but looking back at the save I don't seen any upcoming battle scenarios unless war has been declared during the AI turn...

Scratch that: only when removing the decoupled movement mod does the hang disappear - as you have suspected: should be a loop and not an invalid input or waiting for timeout based on the CPU usage

There are two battle scenarios during the issue:
1. 5 Military transports attack and Outpost and Planetary Defense
2. A huge fleet of everything but the kitchen sink is thrown at an equally large Harpy fleet
The save is thus not useful unless you can use it for some kind of tracing - need a save with only one tactical battle during the issue and small fleets
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

Okay, so I've been doing some debugging, and while I haven't identified the exact cause of the hang, I've had some puzzling results.

First thing I observed was that when I comment out the part of the attack loop that quits the attack behaviour and pushes the attacking ship back onto the move queue, the hang no longer occurs:

Code: Select all

        if invalid_move_target(attack_ship, move_target)
          --has_moved[attack_ship] = nil
          --move_queue[#move_queue+1] = attack_ship
          --goto continue --go back to ship movement
        else
          self:engage_targets(attack_ship, move_target)
          print(attack_ship.name, "engaging targets")

          --check again after attacking
          if invalid_move_target(attack_ship, move_target)
            --has_moved[attack_ship] = nil
            --move_queue[#move_queue+1] = attack_ship
            --goto continue --go back to ship movement
          end
        end
This wasn't too surprising, since without this part the function becomes a straightforward iteration over two queues. The puzzling part is next:

I decided to insert some test conditions that would break out of the entire function (via a return) if either of those if-statements were entered. Instead of just breaking out of the loop though, it would continue iteration 3 more times so that I could see exactly what was happening when control flow took that route (I also added a whole bunch of trace prints):

Code: Select all

  local has_moved = {}
  local move_targets = {}

  local move_queue = sort_move_order(current_sides_ships, self)
  local attack_queue = sort_attack_order(current_sides_ships, self)

  local exit_loop
  local loop_sanity = 0
  while #move_queue > 0 and not battle_is_over() do
    loop_sanity++
    if loop_sanity > 50 error("loop_sanity") end

    -- move a ship
    local move_ship = pop_back(move_queue)
    print(move_ship.name, "moving")

    -- if we are going to retreat this ship, we have to decide that now
    if self:wants_to_retreat(move_ship, general_retreat)
      self:do_retreat(move_ship)
      print(move_ship.name, "retreating")
    else
      local attack_dist = desired_attack_dist(move_ship, self.estimated_tracker_survival)
      if attack_dist
        local move_target = self:choose_move_target(move_ship, attack_dist) or closest_enemy_ship(object_center(move_ship))
        print(move_ship.name, "selecting move target")
        if move_target
          if invalid_move_target(move_ship, move_target)
            print(move_ship.name, move_ship.empire.name, move_target.name)
            print("chose a move target that was invalid")
            move_target = nil
          else
            move_targets[move_ship] = move_target
            self:close_with_target(move_ship, move_target, attack_dist)
            print(move_ship.name, "closing with target")
          end
        end
      end
    end

    has_moved[move_ship] = true

    -- perform attacks, if we can
    local attack_ship = attack_queue[#attack_queue]

    local inner_loop_sanity = 0
    while #attack_queue > 0 and has_moved[attack_ship] and not battle_is_over() do
      inner_loop_sanity++
      if inner_loop_sanity > 50 error("inner_loop_sanity") end

      local move_target = move_targets[attack_ship]
      print(attack_ship.name, "attacking")

      if move_target
        if invalid_move_target(attack_ship, move_target)
          print(attack_ship.name, "move target was invalid")
          if(not exit_loop) exit_loop = 1 end
          --has_moved[attack_ship] = nil
          --move_queue[#move_queue+1] = attack_ship
          --goto continue --go back to ship movement
        else
          self:engage_targets(attack_ship, move_target)
          print(attack_ship.name, "engaging targets")

          --check again after attacking
          if invalid_move_target(attack_ship, move_target)
            print(attack_ship.name, "move target was invalidated")
            if(not exit_loop) exit_loop = 1 end
            --has_moved[attack_ship] = nil
            --move_queue[#move_queue+1] = attack_ship
            --goto continue --go back to ship movement
          end
        end
      else
        --if cloaked, don't attack if we are retreating (since this will decloak us)
        if not (attack_ship.cloaked and attack_ship.retreating)
          self:fire_at_will(attack_ship)
          print(attack_ship.name, "firing at will")
        end
      end

      --advance the attack queue
      pop_back(attack_queue)
      attack_ship = attack_queue[#attack_queue]
    end

    ::continue::

    if exit_loop
      exit_loop++
      print(..exit_loop)
      if exit_loop > 3
        return
      end
    end
  end
Okay, so far so good. I ran it a few times with those lines commented out to make sure my exit_loop code worked as expected, and it did.

Now when I uncomment back in those lines, the game hangs despite the exit loop code! I have to say I really don't understand why this happens.

All three of those lines execute unconditionally. The last line takes control flow to the ::continue:: marker, where it must encounter the exit loop block. Once it loops back around, it must encounter the exit loop block again. There is no mechanism for that block to be skipped.

And the hang is certainly occurring in this loop as commenting out those lines makes the difference between hang and no hang.


Anyways, here is the save file that seems to reproduce the hang fairly reliably (so far).
Attachments
test3.7z
(634.69 KiB) Downloaded 612 times
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

Sven figured it out! Turns out I forgot to handle the case where the attacking ship is itself dead, which is of course possible due to autofire or incoming missiles.

The reason all of my debug attempts were failing was because the hang was not being caused by my while loop as I had thought, deeper in code where the ship assigns weapons.

When a ship fires it's weapons, it assigns a target to each weapon. Since the target for some of its weapons might die before it fires them (e.g. a ship fires 5 weapons at a target but the target dies after 3 of them fire), it keeps looping until it has no more weapons to assign to targets. But if the ship is dead, it still assigns the weapons to the targets, but the weapons never fire... so it never runs out of weapons to fire.

Nothing to do with the control flow of my while loop at all (guess it goes to show how focusing too much on one thing can hurt). Which explains why all my debugging attempts seemed to not work.
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

v1.2 is no longer indev, since the C++ support it needed is now on the public branch.
Attachments
CombatAIMod v1.2 (d08aec4).zip
(22.25 KiB) Downloaded 596 times
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

Update (fb3d720): Fixed an invalid move target error that could occur when a harpy electrocyte was discharged.
Attachments
CombatAIMod v1.2 (fb3d720).zip
(22.36 KiB) Downloaded 599 times
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

...also, I probably should factor in electrocyte when determining a ship's attack power. Those things are insanely deadly.
User avatar
harpy eagle
Posts: 296
Joined: Sat Mar 10, 2018 3:25 am

Re: Tactical Combat AI Mod

Post by harpy eagle »

Now I'm dealing with a bug where carriers sometimes don't attack even though they should, because they think they are too far away from their target.

The strange thing is that the bug appears to be caused by point_dist(object_center(a), object_center(b)) returning a value that is abnormally large.

It's like object center is returning the distance between the target and where the carrier was before it moved. Maybe it's related to the game merging movement of different ships?

I also printed out the results of range_fun.to_target() and it also returns abnormally large values.

I've attached the save that reproduces the bug along with a copy of the exact version of the mod I was using at the time (to ensure the battle plays out exactly the same when you try to reproduce the bug).
Attachments
range_bug.zip
(713.44 KiB) Downloaded 572 times
mod.zip
(22.44 KiB) Downloaded 583 times
User avatar
sven
Site Admin
Posts: 1621
Joined: Sat Jan 31, 2015 10:24 pm
Location: British Columbia, Canada
Contact:

Re: Tactical Combat AI Mod

Post by sven »

harpy eagle wrote: It's like object center is returning the distance between the target and where the carrier was before it moved. Maybe it's related to the game merging movement of different ships?
When you call them in the AI script, tactical actions should execute immediately; so after a call to action.move_ships, the new object_center() value should reflect the new position of any moved ships. (Though there may be some quirks I'm not recalling in the case that the object that moved was destroyed while moving.) Are you certain that the relevant action.move_ships call is in fact being executed prior to the problematic object_center() calls? (I.e., the carriers aren't just entering some sort of queue to be moved?)
Post Reply