|
| 1 | +/* |
| 2 | + * Copyright (C) 2025 The MegaMek Team. All Rights Reserved. |
| 3 | + * |
| 4 | + * This file is part of MegaMek. |
| 5 | + * |
| 6 | + * |
| 7 | + * MegaMek is free software: you can redistribute it and/or modify |
| 8 | + * it under the terms of the GNU General Public License (GPL), |
| 9 | + * version 3 or (at your option) any later version, |
| 10 | + * as published by the Free Software Foundation. |
| 11 | + * |
| 12 | + * MegaMek is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty |
| 14 | + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| 15 | + * See the GNU General Public License for more details. |
| 16 | + * |
| 17 | + * A copy of the GPL should have been included with this project; |
| 18 | + * if not, see <https://www.gnu.org/licenses/>. |
| 19 | + * |
| 20 | + * NOTICE: The MegaMek organization is a non-profit group of volunteers |
| 21 | + * creating free software for the BattleTech community. |
| 22 | + * |
| 23 | + * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks |
| 24 | + * of The Topps Company, Inc. All Rights Reserved. |
| 25 | + * |
| 26 | + * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of |
| 27 | + * InMediaRes Productions, LLC. |
| 28 | + */ |
| 29 | +package megamek.ai.dataset; |
| 30 | + |
| 31 | +import java.util.ArrayList; |
| 32 | +import java.util.EnumMap; |
| 33 | +import java.util.List; |
| 34 | +import java.util.Map; |
| 35 | + |
| 36 | +import megamek.ai.utility.EntityFeatureUtils; |
| 37 | +import megamek.common.Compute; |
| 38 | +import megamek.common.Entity; |
| 39 | +import megamek.common.Game; |
| 40 | +import megamek.common.IAero; |
| 41 | + |
| 42 | +/** |
| 43 | + * Represents the state of a unit. |
| 44 | + * @author Luana Coppio |
| 45 | + */ |
| 46 | +public class UnitStateMap { |
| 47 | + |
| 48 | + /** |
| 49 | + * Enum defining all available unit state fields. |
| 50 | + */ |
| 51 | + public enum Field { |
| 52 | + ID, |
| 53 | + PHASE, |
| 54 | + TEAM_ID, |
| 55 | + ROUND, |
| 56 | + PLAYER_ID, |
| 57 | + CHASSIS, |
| 58 | + MODEL, |
| 59 | + TYPE, |
| 60 | + ROLE, |
| 61 | + X, |
| 62 | + Y, |
| 63 | + FACING, |
| 64 | + MP, |
| 65 | + HEAT, |
| 66 | + PRONE, |
| 67 | + AIRBORNE, |
| 68 | + OFF_BOARD, |
| 69 | + CRIPPLED, |
| 70 | + DESTROYED, |
| 71 | + ARMOR_P, |
| 72 | + INTERNAL_P, |
| 73 | + DONE, |
| 74 | + MAX_RANGE, |
| 75 | + TOTAL_DAMAGE, |
| 76 | + ARMOR, |
| 77 | + INTERNAL, |
| 78 | + BV, |
| 79 | + IS_BOT, |
| 80 | + HAS_ECM, |
| 81 | + ARMOR_FRONT_P, |
| 82 | + ARMOR_LEFT_P, |
| 83 | + ARMOR_RIGHT_P, |
| 84 | + ARMOR_BACK_P, |
| 85 | + WEAPON_DMG_FACING_SHORT_MEDIUM_LONG_RANGE; |
| 86 | + } |
| 87 | + |
| 88 | + |
| 89 | + // Use EnumMap for type safety with our Field enum |
| 90 | + private final Map<Field, Object> data = new EnumMap<>(Field.class); |
| 91 | + // Keep track of insertion order separately |
| 92 | + private final List<Field> fieldOrder = new ArrayList<>(); |
| 93 | + |
| 94 | + /** |
| 95 | + * Creates an empty UnitStateMap. |
| 96 | + */ |
| 97 | + public UnitStateMap() { |
| 98 | + // Initialize with empty map |
| 99 | + } |
| 100 | + |
| 101 | + /** |
| 102 | + * Adds a field to the state map. |
| 103 | + * @param field The field enum |
| 104 | + * @param value The field value |
| 105 | + * @return This UnitStateMap for method chaining |
| 106 | + */ |
| 107 | + public UnitStateMap put(Field field, Object value) { |
| 108 | + if (!data.containsKey(field)) { |
| 109 | + fieldOrder.add(field); |
| 110 | + } |
| 111 | + data.put(field, value); |
| 112 | + return this; |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * Gets a field value from the state map. |
| 117 | + * @param field The field enum |
| 118 | + * @return The field value, or null if not present |
| 119 | + */ |
| 120 | + public Object get(Field field) { |
| 121 | + return data.get(field); |
| 122 | + } |
| 123 | + |
| 124 | + /** |
| 125 | + * Gets a field value with type casting. |
| 126 | + * @param <T> The expected type |
| 127 | + * @param field The field enum |
| 128 | + * @param type The class of the expected type |
| 129 | + * @return The field value cast to the expected type, or null if not present |
| 130 | + */ |
| 131 | + @SuppressWarnings("unchecked") |
| 132 | + public <T> T get(Field field, Class<T> type) { |
| 133 | + Object value = data.get(field); |
| 134 | + if (type.isInstance(value)) { |
| 135 | + return (T) value; |
| 136 | + } |
| 137 | + return null; |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Gets all fields and values in the map. |
| 142 | + * @return The underlying map of data |
| 143 | + */ |
| 144 | + public Map<Field, Object> getAllFields() { |
| 145 | + return new EnumMap<>(data); |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * Gets the ordered list of field enums. |
| 150 | + * @return List of fields in insertion order |
| 151 | + */ |
| 152 | + public List<Field> getFieldOrder() { |
| 153 | + return new ArrayList<>(fieldOrder); |
| 154 | + } |
| 155 | + |
| 156 | + /** |
| 157 | + * Creates a UnitStateMap from an Entity. |
| 158 | + * @param entity The entity to extract state from |
| 159 | + * @param game The game reference |
| 160 | + * @return A populated UnitStateMap |
| 161 | + */ |
| 162 | + public static UnitStateMap fromEntity(Entity entity, Game game) { |
| 163 | + UnitStateMap map = new UnitStateMap(); |
| 164 | + |
| 165 | + // Basic entity information |
| 166 | + map.put(Field.ID, entity.getId()) |
| 167 | + .put(Field.PHASE, game.getPhase()) |
| 168 | + .put(Field.TEAM_ID, entity.getOwner().getTeam()) |
| 169 | + .put(Field.ROUND, game.getCurrentRound()) |
| 170 | + .put(Field.PLAYER_ID, entity.getOwner().getId()) |
| 171 | + .put(Field.CHASSIS, entity.getChassis()) |
| 172 | + .put(Field.MODEL, entity.getModel()) |
| 173 | + .put(Field.TYPE, entity.getClass().getSimpleName()) |
| 174 | + .put(Field.ROLE, entity.getRole()); |
| 175 | + |
| 176 | + // Position and movement |
| 177 | + if (entity.getPosition() != null) { |
| 178 | + map.put(Field.X, entity.getPosition().getX()) |
| 179 | + .put(Field.Y, entity.getPosition().getY()); |
| 180 | + } else { |
| 181 | + map.put(Field.X, -1) |
| 182 | + .put(Field.Y, -1); |
| 183 | + } |
| 184 | + |
| 185 | + map.put(Field.FACING, entity.getFacing()) |
| 186 | + .put(Field.MP, entity.getMpUsedLastRound()) |
| 187 | + .put(Field.HEAT, entity.getHeat()); |
| 188 | + |
| 189 | + // Status flags |
| 190 | + map.put(Field.PRONE, entity.isProne()) |
| 191 | + .put(Field.AIRBORNE, entity.isAirborne()) |
| 192 | + .put(Field.OFF_BOARD, entity.isOffBoard()) |
| 193 | + .put(Field.CRIPPLED, entity.isCrippled()) |
| 194 | + .put(Field.DESTROYED, entity.isDestroyed()) |
| 195 | + .put(Field.DONE, entity.isDone()); |
| 196 | + |
| 197 | + // Health and armor |
| 198 | + map.put(Field.ARMOR_P, entity.getArmorRemainingPercent()) |
| 199 | + .put(Field.INTERNAL_P, entity.getInternalRemainingPercent()) |
| 200 | + .put(Field.ARMOR, entity.getTotalArmor()); |
| 201 | + |
| 202 | + if (entity instanceof IAero aero) { |
| 203 | + map.put(Field.INTERNAL, aero.getSI()); |
| 204 | + } else { |
| 205 | + map.put(Field.INTERNAL, entity.getTotalInternal()); |
| 206 | + } |
| 207 | + |
| 208 | + // Combat stats |
| 209 | + map.put(Field.MAX_RANGE, entity.getMaxWeaponRange()) |
| 210 | + .put(Field.TOTAL_DAMAGE, Compute.computeTotalDamage(entity.getWeaponList())) |
| 211 | + .put(Field.BV, entity.getInitialBV()); |
| 212 | + |
| 213 | + // Equipment and capabilities |
| 214 | + map.put(Field.IS_BOT, entity.getOwner().isBot()) |
| 215 | + .put(Field.HAS_ECM, entity.hasActiveECM()); |
| 216 | + |
| 217 | + // Directional armor |
| 218 | + map.put(Field.ARMOR_FRONT_P, EntityFeatureUtils.getTargetFrontHealthStats(entity)) |
| 219 | + .put(Field.ARMOR_LEFT_P, EntityFeatureUtils.getTargetLeftSideHealthStats(entity)) |
| 220 | + .put(Field.ARMOR_RIGHT_P, EntityFeatureUtils.getTargetRightSideHealthStats(entity)) |
| 221 | + .put(Field.ARMOR_BACK_P, EntityFeatureUtils.getTargetBackHealthStats(entity)); |
| 222 | + |
| 223 | + // Weapon information |
| 224 | + List<Integer> weaponData = new ArrayList<>(); |
| 225 | + entity.getWeaponList().forEach(weapon -> { |
| 226 | + int damage = Compute.computeTotalDamage(weapon); |
| 227 | + int facing = weapon.isRearMounted() ? -entity.getWeaponArc(weapon.getLocation()) : |
| 228 | + entity.getWeaponArc(weapon.getLocation()); |
| 229 | + int shortRange = weapon.getType().getShortRange(); |
| 230 | + int mediumRange = weapon.getType().getMediumRange(); |
| 231 | + int longRange = weapon.getType().getLongRange(); |
| 232 | + |
| 233 | + weaponData.add(damage); |
| 234 | + weaponData.add(facing); |
| 235 | + weaponData.add(shortRange); |
| 236 | + weaponData.add(mediumRange); |
| 237 | + weaponData.add(longRange); |
| 238 | + }); |
| 239 | + map.put(Field.WEAPON_DMG_FACING_SHORT_MEDIUM_LONG_RANGE, weaponData); |
| 240 | + |
| 241 | + return map; |
| 242 | + } |
| 243 | +} |
0 commit comments