Skip to content

Commit fd9393b

Browse files
author
Jasper Geurtz
committed
Add lazy per-frame UnitListCache for filtered unit lists
1 parent 68871f1 commit fd9393b

5 files changed

Lines changed: 136 additions & 59 deletions

File tree

‎src/main/java/bwapi/Cache.java‎

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
package bwapi;
22

3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.function.Function;
9+
import java.util.function.Predicate;
10+
import java.util.function.Supplier;
11+
import java.util.stream.Collectors;
312

413
class Cache<T> {
514
private int frame = -1;
@@ -35,4 +44,83 @@ class BooleanCache extends Cache<Boolean>{}
3544
class OrderCache extends Cache<Order>{}
3645
class UnitTypeCache extends Cache<UnitType>{}
3746
class UpgradeTypeCache extends Cache<UpgradeType>{}
38-
class TechTypeCache extends Cache<TechType>{}
47+
class TechTypeCache extends Cache<TechType>{}
48+
49+
class ConnectedUnitCache {
50+
private int lastUpdate = -1;
51+
private final Map<Unit, List<Unit>> connectedUnits = new HashMap<>();
52+
private final Function<Unit, Unit> condition;
53+
private final Game game;
54+
55+
ConnectedUnitCache(final Game game, final Function<Unit, Unit> condition) {
56+
this.game = game;
57+
this.condition = condition;
58+
}
59+
60+
/**
61+
* Lazily update connectedUnits. Only users of the calls pay for it, and only
62+
* pay once per frame.
63+
* Avoids previous O(n^2) implementation which would be costly for
64+
* lategame carrier fights
65+
*/
66+
List<Unit> getConnected(final Unit unit) {
67+
final int frame = game.getFrameCount();
68+
if (lastUpdate < frame) {
69+
connectedUnits.values().forEach(List::clear);
70+
for (final Unit u : game.getAllUnits()) {
71+
final Unit owner = condition.apply(u);
72+
if (owner != null) {
73+
connectedUnits.computeIfAbsent(owner, k -> new ArrayList<>()).add(u);
74+
}
75+
}
76+
lastUpdate = frame;
77+
}
78+
List<Unit> connected = connectedUnits.get(unit);
79+
return connected == null ?
80+
Collections.emptyList() :
81+
Collections.unmodifiableList(connected);
82+
}
83+
84+
void reset() {
85+
lastUpdate = -1;
86+
connectedUnits.clear();
87+
}
88+
}
89+
90+
/**
91+
* Lazily filters and caches an unmodifiable List<Unit> at most once per frame.
92+
* Only callers of the corresponding getter pay for the filtering, and at most once per frame.
93+
*/
94+
class UnitListCache {
95+
private final Game game;
96+
private final Supplier<List<Unit>> source;
97+
private final Predicate<Unit> filter;
98+
private int lastFrame = -1;
99+
private List<Unit> cached = Collections.emptyList();
100+
101+
UnitListCache(final Game game, final Predicate<Unit> filter) {
102+
this(game, game::getAllUnits, filter);
103+
}
104+
105+
UnitListCache(final Game game, final Supplier<List<Unit>> source, final Predicate<Unit> filter) {
106+
this.game = game;
107+
this.source = source;
108+
this.filter = filter;
109+
}
110+
111+
List<Unit> get() {
112+
final int frame = game.getFrameCount();
113+
if (lastFrame != frame) {
114+
cached = source.get().stream()
115+
.filter(filter)
116+
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
117+
lastFrame = frame;
118+
}
119+
return cached;
120+
}
121+
122+
void reset() {
123+
lastFrame = -1;
124+
cached = Collections.emptyList();
125+
}
126+
}

‎src/main/java/bwapi/ConnectedUnitCache.java‎

Lines changed: 0 additions & 44 deletions
This file was deleted.

‎src/main/java/bwapi/Game.java‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ public class Game {
102102
final ConnectedUnitCache loadedUnitsCache = new ConnectedUnitCache(this, Unit::getTransport);
103103
final ConnectedUnitCache interceptorsCache = new ConnectedUnitCache(this, Unit::getCarrier);
104104
final ConnectedUnitCache larvaCache = new ConnectedUnitCache(this, Unit::getHatchery);
105+
private final UnitListCache minerals = new UnitListCache(this, u -> u.getType().isMineralField());
106+
private final UnitListCache geysers = new UnitListCache(this, u -> u.getType() == Resource_Vespene_Geyser);
107+
private final UnitListCache neutralUnits = new UnitListCache(this, u -> u.getPlayer().equals(neutral()));
108+
private final UnitListCache selfPylons = new UnitListCache(this, () -> self().getUnits(), u -> u.getType() == Protoss_Pylon);
105109

106110
final SideEffectQueue sideEffects = new SideEffectQueue();
107111

@@ -155,6 +159,10 @@ void init() {
155159
loadedUnitsCache.reset();
156160
interceptorsCache.reset();
157161
larvaCache.reset();
162+
minerals.reset();
163+
geysers.reset();
164+
neutralUnits.reset();
165+
selfPylons.reset();
158166

159167
final int forceCount = gameData().getForceCount();
160168
forces = new Force[forceCount];
@@ -368,9 +376,7 @@ public List<Unit> getAllUnits() {
368376
* @return List<Unit> containing @minerals
369377
*/
370378
public List<Unit> getMinerals() {
371-
return getAllUnits().stream()
372-
.filter(u -> u.getType().isMineralField())
373-
.collect(Collectors.toList());
379+
return minerals.get();
374380
}
375381

376382
/**
@@ -379,9 +385,7 @@ public List<Unit> getMinerals() {
379385
* @return List<Unit> containing @geysers
380386
*/
381387
public List<Unit> getGeysers() {
382-
return getAllUnits().stream()
383-
.filter(u -> u.getType() == Resource_Vespene_Geyser)
384-
.collect(Collectors.toList());
388+
return geysers.get();
385389
}
386390

387391
/**
@@ -391,9 +395,7 @@ public List<Unit> getGeysers() {
391395
* @return List<Unit> containing all neutral units.
392396
*/
393397
public List<Unit> getNeutralUnits() {
394-
return getAllUnits().stream()
395-
.filter(u -> u.getPlayer().equals(neutral()))
396-
.collect(Collectors.toList());
398+
return neutralUnits.get();
397399
}
398400

399401
/**
@@ -1018,7 +1020,7 @@ public boolean hasPowerPrecise(final int x, final int y) {
10181020
* @return true if the type at the given position will have power, false if the type at the given position will be unpowered.
10191021
*/
10201022
public boolean hasPowerPrecise(final int x, final int y, final UnitType unitType) {
1021-
return isValidPosition(x, y) && hasPower(x, y, unitType, self().getUnits().stream().filter(u -> u.getType() == Protoss_Pylon).collect(Collectors.toList()));
1023+
return isValidPosition(x, y) && hasPower(x, y, unitType, selfPylons.get());
10221024
}
10231025

10241026
public boolean hasPowerPrecise(final Position position) {

‎src/main/java/bwapi/Player.java‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import java.util.List;
66
import java.util.Objects;
7-
import java.util.stream.Collectors;
87

98
import static bwapi.UnitType.*;
109
import static bwapi.UpgradeType.*;
@@ -28,6 +27,7 @@ public class Player implements Comparable<Player> {
2827
private final PlayerType playerType;
2928
private final TilePosition startLocation;
3029
private final Color color;
30+
private final UnitListCache units;
3131
private PlayerSelf self = null;
3232
PlayerSelf self() {
3333
if (self == null) {
@@ -44,6 +44,7 @@ PlayerSelf self() {
4444
this.playerType = PlayerType.idToEnum[playerData.getType()];
4545
this.startLocation = new TilePosition(playerData.getStartLocationX(), playerData.getStartLocationY());
4646
this.color = new Color(playerData.getColor());
47+
this.units = new UnitListCache(game, u -> equals(u.getPlayer()));
4748
}
4849

4950
/**
@@ -77,9 +78,7 @@ public String getName() {
7778
* This does not include units that are loaded into transports, @Bunkers, @Refineries, @Assimilators, or @Extractors.
7879
*/
7980
public List<Unit> getUnits() {
80-
return game.getAllUnits().stream()
81-
.filter(u -> equals(u.getPlayer()))
82-
.collect(Collectors.toList());
81+
return units.get();
8382
}
8483

8584
/**

‎src/test/java/bwapi/GameTest.java‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static org.junit.Assert.assertEquals;
1818
import static org.junit.Assert.assertFalse;
1919
import static org.junit.Assert.assertNull;
20+
import static org.junit.Assert.assertSame;
2021
import static org.junit.Assert.assertTrue;
2122
import static org.mockito.BDDMockito.given;
2223
import static org.mockito.Mockito.doReturn;
@@ -210,4 +211,35 @@ public void onFrameShouldRefreshPlayersAndExcludeDefeatedOrLeftPlayers() {
210211

211212
assertTrue(game.enemies().isEmpty());
212213
}
214+
215+
@Test
216+
public void unitListCachesShouldBeLazyAndCachedPerFrame() throws IOException {
217+
Game game = GameBuilder.createGame();
218+
Player self = game.self();
219+
Player neutral = game.neutral();
220+
221+
Unit mineral = mock(Unit.class);
222+
when(mineral.getType()).thenReturn(UnitType.Resource_Mineral_Field);
223+
when(mineral.getPlayer()).thenReturn(neutral);
224+
225+
Unit geyser = mock(Unit.class);
226+
when(geyser.getType()).thenReturn(UnitType.Resource_Vespene_Geyser);
227+
when(geyser.getPlayer()).thenReturn(neutral);
228+
229+
Unit pylon = mock(Unit.class);
230+
when(pylon.getType()).thenReturn(UnitType.Protoss_Pylon);
231+
when(pylon.getPlayer()).thenReturn(self);
232+
233+
game.setAllUnits(Arrays.asList(mineral, geyser, pylon));
234+
235+
assertEquals(Collections.singletonList(mineral), game.getMinerals());
236+
assertSame(game.getMinerals(), game.getMinerals());
237+
assertEquals(Collections.singletonList(geyser), game.getGeysers());
238+
assertSame(game.getGeysers(), game.getGeysers());
239+
assertEquals(Arrays.asList(mineral, geyser), game.getNeutralUnits());
240+
assertSame(game.getNeutralUnits(), game.getNeutralUnits());
241+
assertEquals(Collections.singletonList(pylon), self.getUnits());
242+
assertSame(self.getUnits(), self.getUnits());
243+
assertEquals(Arrays.asList(mineral, geyser), neutral.getUnits());
244+
}
213245
}

0 commit comments

Comments
 (0)