Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] Proof of Concept of event-based kill assignment #4781

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Sleet01
Copy link
Collaborator

@Sleet01 Sleet01 commented Sep 15, 2023

Event-based Kill Tracking Proof of Concept

Summary

This is the rough initial attempt to implement some measure of Event-based kill claiming.
I'd like to have this looked at, and if it seems feasible at scale, implement a more robust version.

Details

The basic gist is:

  • There are too many points at which we need to track damage cause and effect for linear, sequential damage tracking alone to be workable.
  • There are also circumstances where it is difficult to track the effects of an action or PSR forward through to unit destruction, such as a physical attack that causes one enemy to deal physical damage to another, causing the second enemy's destruction.

Rather than tracking damage and attackers down into the multifarious damage resolution methods and special cases, I believe using event handling simplifies this problem considerably:

  • When damage is dealt (or when an entity's attacks are registered), a "Unit has died" Listener can be created. As it is created concurrently with the damage itself, information about the attack / damage dealt, including the dealer, type, intended target, etc. can be easily stored in the listener.
  • When a unit is destroyed, a "Unit has died" Event can be dispatched. The event knows who has died, and generally why, but due to how our damage code is organized, likely does not know who killed it1.
  • When an event is dispatched, all current Listeners can examine the event to see if they might be responsible for the kill, and if so, claim it for the unit whose attack dispatched them originally2. If not, they can ignore it.
  • At some point, either the end of a phase or the end of a round, all Listeners can be wiped, readying the game for the next round of attacks and (possible) fatalities.

Because destruction must chronologically follow attacks or actions - there is no effect without a cause - this approach of registering Causes and then letting them decide which one is responsible for a later Effect assures us that we will not credit later Attacks for prior Kills, something that could happen with our depth-first approach to damage generation and limited tracking now. Conversely, while the linear approach to tracking damage to subsequent kills requires tracing multiple paths to confirm, say, that nobody died, the event-based approach can simply do nothing if nothing dies; it's quite economical.

Proof of Concept

This extremely rough POC adds only unit-level kill credit for:

  1. direct weapon attacks,
  2. ejections,
  3. crew kills,
  4. generic "destruction"

It makes no attempt to resolve disputes over kill claims, awarding credit to any units that can "prove" that they attacked the dying unit. It cannot deal with long-range artillery, or accurately accumulate damage dealt over several turns. There are a number of methods of destruction it is not instrumented for. It's janky3.
Most importantly, it only claims kills for units, not for the team they belong to.

But I can already see how this approach would simplify kill accounting without requiring onerous levels of plumbing. For instance, to add kill-sharing for TAG scouts and missile boats, we would need only to add the following lines to the TAG-handling code:

            registerDiedListener(ah.getAttacker(), ah.waa);

somewhere around line 34,342 (!?) of GameManager.java. That should be all that's needed to give TAGgers credit for helping with kills.

Full Implementation

I believe a full implementation will require rewriting the existing kill-tracking code completely; if part of it uses Events, it all should, for consistency and clarity. In writing the POC I didn't even find the final victory scoring code, so I can't say yet how much work it will require to replace. But it should be on the order of adding one or two lines in a few dozen places throughout GameManager.java, with a more substantial rework of victory scoring and implementation of a map of Unit->Kill Claims objects to tally damage dealt and claimed kills over the course of a game.

I'd also say that tracking criteria for awarding kills (per unit or per team) should be split out of the GameUnitDeathListenerAdapter implementation, for a few reasons. While event handlers are terrific for ignoring everything except the specific information they are interested in, there are a few things they're ill-suited to:

  1. comparisons with other Listener instances: two different units' attacks may both think they killed a dying target, but they can't see or talk to each other so by themselves they can't determine who "wins", by damage or some other metric.
  2. heavy calculations in general: Java's docs recommends keeping Events and EventListeners as lightweight as possible, as they will be interrupting the same thread when fired and handled.
  3. sticking around. Because the EventListeners all live in Game, there's no guarantee that they'll be around from round to round. In fact, they should be cleared from time to time, so no accumulated state should live in them.

That said, the simple Listener-clearing function I used in the POC should be replaced by a method of self-deleting4, within the listeners themselves, based on a timeframe and/or count of deaths:

  • A Listener may only last one Phase: perhaps we only want to track Physical Attack kills within that phase, for instance.
  • A Listener may last one Round: we may wish to let an Indirect Artillery attack claim deaths from damage and from failed PSRs all the way through from the Indirect phase to the end of the round.
  • A Listener may last multiple Rounds: off-board Artillery attacks with non-zero flight times should at least stick around until their arrival Round, to see what happens.
  • We may want a given Listener to only try to claim one kill maximum: in the TAG example, perhaps we only share credit on the directly-tagged target, while the IF unit also gets credit for any collateral kills in the AE template.
    etc.

Concerns

There are a lot of unknowns and provisos here:

  • I have no idea how using this system for multiple 10s of units with between 0 and possibly dozens of attacks per turn will perform, for instance.
  • Thread safety is a concern (although Java should enforce that only Listeners in the same thread as Event dispatchers can see those Events).
  • Memory usage could get higher if we are not careful.
  • Reasoning about the event-handling system is difficult until one has experience with it.

Future

I could see us replacing a lot of fiddly multi-stage unit updating and nested if/else/switch/case code with fired events and simple handlers; this paradigm seems particularly well-suited to board games with limited numbers of units that need to deal with a wealth of different rules.

For instance, we could move a lot of the spaghetti code out of GameManager.java and into the individual unit Types. Green Gas attacks could just fire an Event that only units with old TSM would need to listen for. This approach is especially nice as an alternative to repeatedly polling each and every unit for location in the case of, e.g., an AE weapon. Well, code-wise it's nicer; under the hood it's doing much the same thing.

Footnotes

  1. There are some members in Entity that appear to be used for tracking who last attacked a unit and with what, but there is no guarantee that they will be set properly and in many cases the source of damage is a special-case handler function in some weapon handler code, or GameManager.java itself. Additionally, they do not allow for much information: we might know that Atlas A dealt 10 damage to Crusader C most recently, but lose the information that Blackjack B dealt 20 damage immediately prior, so can only give the kill to A.
  2. In the POC, multiple attacks can claim the same kill for their dealer entity; in a full implementation, a leaderboard for each kill, managed by the Game or GameManager and compiled at the end of the game, would likely be necessary for more accuracy.
  3. The amount of information each event carries is probably overkill, and needing no-op handlers in the various sub-windows that inherit or implement GameListener Feels Bad. This, however, can all be tuned.
  4. I have already tested this approach, but did not implement it for the POC as it would require adding more fields to the Listener.

Media

I still want to do more testing, but here are files from a test game with the POC code bolted on to the side of the existing kill claim code. It seems to be working pretty well: although the majority of "deaths" came from auto-ejecting pilots, the kills were assigned (afiact) correctly to the units who made the attacks that caused those ejections. Dead units may still be listed as killed by "pilot error", but replacing that code with events would fix that.

megamek.log

gamelog.tar.gz

salvage.mul.tar.gz

Bot_Princess.mul.tar.gz

20230914_aftermath_of_2xShiva_vs_12_x_Gurkha_fight

@Sleet01 Sleet01 self-assigned this Sep 15, 2023
@Sleet01 Sleet01 added Needs Investigation This issue needs investigation and/or triage. Refactoring Major This will require major changes across the project. In Development (Draft) An additional way to mark something as a draft. Make it stand out more. labels Sep 15, 2023
@Sleet01 Sleet01 marked this pull request as draft September 15, 2023 08:01
@pheonixstorm
Copy link
Collaborator

Easy kill chains would be ammo and engine explosions. A kills B, unit explodes and kills C & D which kills E. Count kills and reset killed by for non team kills.

I know ejection kills would be a lot harder unless you can clue on what would be the cause of an ejection or unit abandonment. Getting legged, engine destroyed, gyro destroyed, or any other type of mobility kill for vehicles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Development (Draft) An additional way to mark something as a draft. Make it stand out more. Major This will require major changes across the project. Needs Investigation This issue needs investigation and/or triage. Refactoring
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants