Skip to content

Commit 431ab09

Browse files
committed
fix: refactoring unitState object and serializer
1 parent b018e1a commit 431ab09

File tree

5 files changed

+395
-24
lines changed

5 files changed

+395
-24
lines changed

megamek/src/megamek/ai/dataset/TabSeparatedValueSerializer.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
*/
3939
public abstract class TabSeparatedValueSerializer<T> {
4040

41-
protected final DecimalFormat LOG_DECIMAL = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.US));
41+
protected static final DecimalFormat LOG_DECIMAL = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.US));
4242

4343
/**
4444
* Serializes an object to TSV format.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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.Arrays;
33+
import java.util.HashMap;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.function.Function;
37+
import java.util.stream.Collectors;
38+
39+
import megamek.ai.dataset.UnitStateMap.Field;
40+
import megamek.common.UnitRole;
41+
import megamek.common.enums.GamePhase;
42+
43+
/**
44+
* <p>Serializer for UnitStateMap to TSV format.</p>
45+
* <p>Uses a flexible map-based approach with enum fields.</p>
46+
* @author Luana Coppio
47+
*/
48+
public class UnitStateMapSerializer extends TabSeparatedValueSerializer<UnitStateMap> {
49+
50+
// Define the default field order for serialization
51+
private final List<Field> fieldOrder;
52+
53+
private static final Map<Class<?>, Function<Object, String>> FORMAT_HANDLERS = new HashMap<>();
54+
static {
55+
FORMAT_HANDLERS.put(Boolean.class, value -> (Boolean) value ? "1" : "0");
56+
FORMAT_HANDLERS.put(boolean.class, value -> (Boolean) value ? "1" : "0");
57+
FORMAT_HANDLERS.put(Double.class, LOG_DECIMAL::format);
58+
FORMAT_HANDLERS.put(double.class, LOG_DECIMAL::format);
59+
FORMAT_HANDLERS.put(Float.class, LOG_DECIMAL::format);
60+
FORMAT_HANDLERS.put(float.class, LOG_DECIMAL::format);
61+
FORMAT_HANDLERS.put(GamePhase.class, value -> ((GamePhase) value).name());
62+
FORMAT_HANDLERS.put(UnitRole.class, value -> value == null ? UnitRole.NONE.name() : ((UnitRole) value).name());
63+
FORMAT_HANDLERS.put(List.class, value -> ((List<?>) value).stream()
64+
.map(String::valueOf)
65+
.collect(Collectors.joining(" ")));
66+
}
67+
68+
/**
69+
* Creates a serializer with default field order.
70+
*/
71+
public UnitStateMapSerializer() {
72+
// Use all fields in their enum order
73+
this.fieldOrder = Arrays.asList(Field.values());
74+
}
75+
76+
/**
77+
* Creates a serializer with custom field order.
78+
* @param fieldOrder The desired order of fields for serialization
79+
*/
80+
public UnitStateMapSerializer(List<Field> fieldOrder) {
81+
this.fieldOrder = new ArrayList<>(fieldOrder);
82+
}
83+
84+
@Override
85+
public String serialize(UnitStateMap stateMap) {
86+
List<String> values = new ArrayList<>(fieldOrder.size());
87+
88+
// Get all fields from the map
89+
Map<Field, Object> allFields = stateMap.getAllFields();
90+
91+
// Process each field in the defined order
92+
for (Field field : fieldOrder) {
93+
Object value = allFields.get(field);
94+
95+
// Handle null values
96+
if (value == null) {
97+
// Special case for UnitRole
98+
if (field == Field.ROLE) {
99+
values.add(UnitRole.NONE.name());
100+
} else {
101+
values.add("");
102+
}
103+
continue;
104+
}
105+
106+
// Get the class of the value
107+
Class<?> type = value.getClass();
108+
109+
// Use format handler if available for this type
110+
if (FORMAT_HANDLERS.containsKey(type)) {
111+
values.add(FORMAT_HANDLERS.get(type).apply(value));
112+
} else if (type.isEnum()) {
113+
values.add(((Enum<?>) value).name());
114+
} else if (List.class.isAssignableFrom(type)) {
115+
values.add(FORMAT_HANDLERS.get(List.class).apply(value));
116+
} else {
117+
values.add(String.valueOf(value));
118+
}
119+
}
120+
121+
return String.join("\t", values);
122+
}
123+
124+
@Override
125+
public String getHeaderLine() {
126+
return fieldOrder.stream()
127+
.map(Field::name)
128+
.collect(Collectors.joining("\t"));
129+
}
130+
}

0 commit comments

Comments
 (0)