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

feat: slightly better serializer for unit actions and state, added attack action too #6834

Merged
merged 16 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions megamek/src/megamek/ai/dataset/ActionAndState.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,28 @@
*/
package megamek.ai.dataset;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;

/**
* <p>Abstract class to serialize/deserialize objects to/from TSV format.</p>
* <p>It does not have a fromTsv function because I could not find a way to make a good API for it.</p>
* @param <T> type of object to serialize/deserialize
* Abstract class to serialize objects to TSV format.
* @author Luana Coppio
*/
public abstract class TsvSerde<T> {
public abstract class TabSeparatedValueSerializer<T> {

protected final DecimalFormat LOG_DECIMAL = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.US));

/**
* Serializes an object to TSV format.
* @param obj object to serialize
* @return the object serialized in TSV format
* @return the serialized string formatted as a tab separated value entry
*/
public abstract String toTsv(T obj);
public abstract String serialize(T obj);

/**
* Returns the header line for the TSV format.
* @return the header line
* @return the header line for the tab separated value entry
*/
public abstract String getHeaderLine();
}
133 changes: 71 additions & 62 deletions megamek/src/megamek/ai/dataset/UnitAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,80 +27,89 @@
*/
package megamek.ai.dataset;

import java.util.List;
import java.util.stream.Stream;

import megamek.ai.utility.EntityFeatureUtils;
import megamek.client.ui.SharedUtility;
import megamek.common.Coords;
import megamek.common.Compute;
import megamek.common.Entity;
import megamek.common.IAero;
import megamek.common.MovePath;
import megamek.common.MoveStep;

import java.util.List;
import megamek.common.UnitRole;

/**
* Represents a unit action.
* @param id unit id
* @param facing facing direction
* @param fromX x coordinate of the starting position
* @param fromY y coordinate of the starting position
* @param toX x coordinate of the final position
* @param toY y coordinate of the final position
* @param hexesMoved number of hexes moved
* @param distance distance moved
* @param mpUsed movement points used
* @param maxMp maximum movement points
* @param mpP movement points percentage
* @param heatP heat percentage
* @param armorP armor percentage
* @param internalP internal percentage
* @param jumping jumping
* @param prone prone
* @param legal is move legal
* UnitAction is a record that represents the action of a unit in the game.
* @author Luana Coppio
*/
public record UnitAction(int id, int teamId, int playerId, String chassis, String model, int facing, int fromX, int fromY, int toX, int toY, int hexesMoved, int distance, int mpUsed,
int maxMp, double mpP, double heatP, double armorP, double internalP, boolean jumping, boolean prone,
boolean legal, double chanceOfFailure, List<MovePath.MoveStepType> steps, boolean bot) {
public record UnitAction(int id, int teamId, int playerId, String chassis, String model, int facing,
int fromX, int fromY, int toX, int toY, int hexesMoved, int distance,
int mpUsed, int maxMp, double mpP, double heatP, double armorP, double internalP,
boolean jumping, boolean prone, boolean legal, double chanceOfFailure,
List<MovePath.MoveStepType> steps, boolean bot,
boolean hasEcm, int armor, int internal, int bv, int maxRange, int totalDamage,
float armorFrontP, float armorLeftP, float armorRightP, float armorBackP, UnitRole role,
List<Integer> weaponDamageFacingShortMediumLongRange) {

public static UnitAction fromMovePath(MovePath movePath) {
/**
* Creates a UnitAction from a MovePath.
* @param movePath The MovePath to convert
* @return The UnitAction
*/
public static UnitAction fromMovePath(MovePath movePath) {
Entity entity = movePath.getEntity();
double chanceOfFailure = SharedUtility.getPSRList(movePath).stream().map(psr -> psr.getValue() / 36d).reduce(0.0, (a, b) -> a * b);
double chanceOfFailure = SharedUtility.getPSRList(movePath).stream()
.map(psr -> psr.getValue() / 36d)
.reduce(0.0, (a, b) -> a * b);

var steps = movePath.getStepVector().stream().map(MoveStep::getType).toList();

return new UnitAction(
entity.getId(),
entity.getOwner() != null ? entity.getOwner().getTeam() : -1,
entity.getOwner() != null ? entity.getOwner().getId() : -1,
entity.getChassis(),
entity.getModel(),
movePath.getFinalFacing(),
movePath.getStartCoords() != null ? movePath.getStartCoords().getX() : -1,
movePath.getStartCoords() != null ? movePath.getStartCoords().getY() : -1,
movePath.getFinalCoords() != null ? movePath.getFinalCoords().getX() : -1,
movePath.getFinalCoords() != null ? movePath.getFinalCoords().getY() : -1,
movePath.getHexesMoved(),
movePath.getDistanceTravelled(),
movePath.getMpUsed(),
movePath.getMaxMP(),
movePath.getMaxMP() > 0 ? (double) movePath.getMpUsed() / movePath.getMaxMP() : 0.0,
entity.getHeatCapacity() > 0 ? entity.getHeat() / (double) entity.getHeatCapacity() : 0.0,
entity.getArmorRemainingPercent(),
entity.getInternalRemainingPercent(),
movePath.isJumping(),
movePath.getFinalProne(),
movePath.isMoveLegal(),
chanceOfFailure,
steps,
entity.getOwner().isBot()
entity.getId(),
entity.getOwner() != null ? entity.getOwner().getTeam() : -1,
entity.getOwner() != null ? entity.getOwner().getId() : -1,
entity.getChassis(),
entity.getModel(),
movePath.getFinalFacing(),
movePath.getStartCoords() != null ? movePath.getStartCoords().getX() : -1,
movePath.getStartCoords() != null ? movePath.getStartCoords().getY() : -1,
movePath.getFinalCoords() != null ? movePath.getFinalCoords().getX() : -1,
movePath.getFinalCoords() != null ? movePath.getFinalCoords().getY() : -1,
movePath.getHexesMoved(),
movePath.getDistanceTravelled(),
movePath.getMpUsed(),
movePath.getMaxMP(),
movePath.getMaxMP() > 0 ? (double) movePath.getMpUsed() / movePath.getMaxMP() : 0.0,
entity.getHeatCapacity() > 0 ? entity.getHeat() / (double) entity.getHeatCapacity() : 0.0,
entity.getArmorRemainingPercent(),
entity.getInternalRemainingPercent(),
movePath.isJumping(),
movePath.getFinalProne(),
movePath.isMoveLegal(),
chanceOfFailure,
steps,
entity.getOwner().isBot(),
entity.hasActiveECM(),
entity.getTotalArmor(),
entity instanceof IAero aero ? aero.getSI() : entity.getTotalInternal(),
entity.getInitialBV(),
entity.getMaxWeaponRange(),
Compute.computeTotalDamage(entity.getWeaponList()),
EntityFeatureUtils.getTargetFrontHealthStats(entity),
EntityFeatureUtils.getTargetLeftSideHealthStats(entity),
EntityFeatureUtils.getTargetRightSideHealthStats(entity),
EntityFeatureUtils.getTargetBackHealthStats(entity),
entity.getRole(),
entity.getWeaponList().stream().flatMap(weapon -> {
int damage = Compute.computeTotalDamage(weapon);
int facing = weapon.isRearMounted() ? -entity.getWeaponArc(weapon.getLocation()) :
entity.getWeaponArc(weapon.getLocation());
int shortRange = weapon.getType().getShortRange();
int mediumRange = weapon.getType().getMediumRange();
int longRange = weapon.getType().getLongRange();
return Stream.of(damage, facing, shortRange, mediumRange, longRange);
}).toList()
);
}

public Coords currentPosition() {
return new Coords(fromX, fromY);
}

public boolean isHuman() {
return !bot;
}

public Coords finalPosition() {
return new Coords(toX, toY);
}
}
27 changes: 20 additions & 7 deletions megamek/src/megamek/ai/dataset/UnitActionField.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,20 @@ public enum UnitActionField {
STEPS("STEPS"),
TEAM_ID("TEAM_ID"),
CHANCE_OF_FAILURE("CHANCE_OF_FAILURE"),
IS_BOT("IS_BOT");
IS_BOT("IS_BOT"),
HAS_ECM("HAS_ECM"),
ARMOR("ARMOR"),
INTERNAL("INTERNAL"),
BV("BV"),
MAX_RANGE("MAX_RANGE"),
ARMOR_FRONT_P("ARMOR_FRONT_P"),
ARMOR_LEFT_P("ARMOR_LEFT_P"),
ARMOR_RIGHT_P("ARMOR_RIGHT_P"),
ARMOR_BACK_P("ARMOR_BACK_P"),
TOTAL_DAMAGE("TOTAL_DAMAGE"),
ROLE("ROLE"),
WEAPON_DMG_FACING_SHORT_MEDIUM_LONG_RANGE("WEAPON_DMG_FACING_SHORT_MEDIUM_LONG_RANGE"),
;

private final String headerName;

Expand All @@ -71,18 +84,18 @@ public String getHeaderName() {
*/
public static String getHeaderLine() {
return Arrays.stream(values())
.map(UnitActionField::getHeaderName)
.collect(Collectors.joining("\t"));
.map(UnitActionField::getHeaderName)
.collect(Collectors.joining("\t"));
}

/**
* Builds the TSV header line (joined by tabs) by iterating over all enum constants.
*/
public static String getPartialHeaderLine(int startsAt, int endsAt) {
return Arrays.stream(values())
.skip(startsAt)
.limit(endsAt - startsAt)
.map(UnitActionField::getHeaderName)
.collect(Collectors.joining("\t"));
.skip(startsAt)
.limit(endsAt - startsAt)
.map(UnitActionField::getHeaderName)
.collect(Collectors.joining("\t"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,18 @@
*/
package megamek.ai.dataset;

import megamek.common.MovePath;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import megamek.common.UnitRole;

/**
* <p>serializer and deserializer for UnitAction to/from TSV format.</p>
* @author Luana Coppio
*/
public class UnitActionSerde extends TsvSerde<UnitAction> {

private final DecimalFormat LOG_DECIMAL =
new DecimalFormat("0.00", DecimalFormatSymbols.getInstance());
public class UnitActionSerializer extends TabSeparatedValueSerializer<UnitAction> {

@Override
public String toTsv(UnitAction obj) {
public String serialize(UnitAction obj) {
String[] row = new String[UnitActionField.values().length];
row[UnitActionField.ENTITY_ID.ordinal()] = String.valueOf(obj.id());
row[UnitActionField.PLAYER_ID.ordinal()] = String.valueOf(obj.playerId());
Expand All @@ -70,11 +63,24 @@ public String toTsv(UnitAction obj) {
row[UnitActionField.LEGAL.ordinal()] = obj.legal() ? "1" : "0";
row[UnitActionField.CHANCE_OF_FAILURE.ordinal()] = LOG_DECIMAL.format(obj.chanceOfFailure());
row[UnitActionField.IS_BOT.ordinal()] = obj.bot() ? "1" : "0";

// For STEPS, join the list of MoveStepType values with a space.
row[UnitActionField.STEPS.ordinal()] = obj.steps().stream()
.map(Enum::name)
.collect(Collectors.joining(" "));
.map(Enum::name)
.collect(Collectors.joining(" "));
row[UnitActionField.HAS_ECM.ordinal()] = obj.hasEcm() ? "1" : "0";
row[UnitActionField.ARMOR.ordinal()] = String.valueOf(obj.armor());
row[UnitActionField.INTERNAL.ordinal()] = String.valueOf(obj.internal());
row[UnitActionField.BV.ordinal()] = String.valueOf(obj.bv());
row[UnitActionField.MAX_RANGE.ordinal()] = String.valueOf(obj.maxRange());
row[UnitActionField.TOTAL_DAMAGE.ordinal()] = String.valueOf(obj.totalDamage());
row[UnitActionField.ARMOR_FRONT_P.ordinal()] = LOG_DECIMAL.format(obj.armorFrontP());
row[UnitActionField.ARMOR_LEFT_P.ordinal()] = LOG_DECIMAL.format(obj.armorLeftP());
row[UnitActionField.ARMOR_RIGHT_P.ordinal()] = LOG_DECIMAL.format(obj.armorRightP());
row[UnitActionField.ARMOR_BACK_P.ordinal()] = LOG_DECIMAL.format(obj.armorBackP());
row[UnitActionField.ROLE.ordinal()] = obj.role() == null ? UnitRole.NONE.name() : obj.role().name();
row[UnitActionField.WEAPON_DMG_FACING_SHORT_MEDIUM_LONG_RANGE.ordinal()] =
obj.weaponDamageFacingShortMediumLongRange().stream()
.map(Object::toString)
.collect(Collectors.joining(" "));

return String.join("\t", row);
}
Expand Down
Loading