From bfb33aab8675c1ae81225646878ad594490baea4 Mon Sep 17 00:00:00 2001 From: Jasper Geurtz Date: Sat, 7 Nov 2026 20:38:06 +0100 Subject: [PATCH] Align JBWAPI behavior and bug fixes with C++ BWAPI 4.4.0 - CommandTemp: subtract gas cost on Upgrade; fix Cancel_Train_Slot queue shift OOB - Unit: fix canBuildAddon(UnitType) overload call, canAttackGrouped(Position) check, isTargetable visibility check, canTargetUnit(Unit) default checkCommandibility, isVisible replay fallback/bounds check, and build/train/useTech special-case redirects - Bullet: fix isVisible replay fallback and player ID bounds check - Player: fix isEnemy/isNeutral delegation to PlayerData and supplyTotal/supplyUsed Race.None and bounds checks - Region: fix updateNeighbours accessibility check to compare regionGroupID - Game: refresh allies/enemies/observers each frame and fix drawEllipseScreen coordinate type - BuildingPlacer: avoid mutating original PlacementReserve in ReserveDefault - UnitCommand: include Repair in isQueued - Add unit tests for all BWAPI 4.4.0 alignment fixes --- src/main/java/bwapi/BuildingPlacer.java | 11 +- src/main/java/bwapi/Bullet.java | 14 ++- src/main/java/bwapi/CommandTemp.java | 4 +- src/main/java/bwapi/Game.java | 148 ++++++++++++----------- src/main/java/bwapi/Player.java | 23 +++- src/main/java/bwapi/Race.java | 6 + src/main/java/bwapi/Region.java | 2 +- src/main/java/bwapi/Unit.java | 61 +++++++--- src/main/java/bwapi/UnitCommand.java | 3 + src/test/java/bwapi/BulletTest.java | 32 +++++ src/test/java/bwapi/CommandTempTest.java | 64 ++++++++++ src/test/java/bwapi/GameTest.java | 36 ++++++ src/test/java/bwapi/PlayerTest.java | 43 +++++++ src/test/java/bwapi/RegionTest.java | 53 ++++++++ src/test/java/bwapi/UnitTest.java | 111 +++++++++++++++++ 15 files changed, 510 insertions(+), 101 deletions(-) create mode 100644 src/test/java/bwapi/BulletTest.java create mode 100644 src/test/java/bwapi/CommandTempTest.java create mode 100644 src/test/java/bwapi/RegionTest.java diff --git a/src/main/java/bwapi/BuildingPlacer.java b/src/main/java/bwapi/BuildingPlacer.java index 4eb40155..de8f8d9e 100644 --- a/src/main/java/bwapi/BuildingPlacer.java +++ b/src/main/java/bwapi/BuildingPlacer.java @@ -260,7 +260,7 @@ static void ReserveExistingAddonPlacement(final PlacementReserve reserve, final static void ReserveDefault(final PlacementReserve reserve, final UnitType type, final TilePosition desiredPosition, final Game game) { reserve.backup(); - final PlacementReserve original = reserve; + final PlacementReserve original = new PlacementReserve(reserve); // Reserve some space around some specific units for (final Unit it : game.self().getUnits()) { @@ -368,6 +368,15 @@ static class PlacementReserve { backup(); } + PlacementReserve(final PlacementReserve other) { + maxSearch = other.maxSearch; + reset(); + for (int i = 0; i < MAX_RANGE; i++) { + System.arraycopy(other.data[i], 0, data[i], 0, MAX_RANGE); + System.arraycopy(other.save[i], 0, save[i], 0, MAX_RANGE); + } + } + // Checks if the given x/y value is valid for the Placement position static boolean isValidPos(final int x, final int y) { return x >= 0 && x < MAX_RANGE && y >= 0 && y < MAX_RANGE; diff --git a/src/main/java/bwapi/Bullet.java b/src/main/java/bwapi/Bullet.java index 0c8ef6cc..d935b78f 100644 --- a/src/main/java/bwapi/Bullet.java +++ b/src/main/java/bwapi/Bullet.java @@ -180,11 +180,17 @@ public boolean isVisible() { * @param player If this parameter is specified, then the Bullet's visibility to the given player is checked. If this parameter is omitted, then a default value of null is used, which will check if the BWAPI player has vision of the {@link Bullet}. * @return true if the {@link Bullet} is visible to the specified player, false if the {@link Bullet} is not visible to the specified player. */ - public boolean isVisible(final Player player) { - if (player == null) { - return isVisible(); + public boolean isVisible(Player player) { + player = player == null ? game.self() : player; + if (player != null) { + return player.hasValidId() && bulletData.isVisible(player.getID()); } - return bulletData.isVisible(player.getID()); + for (int i = 0; i < Player.MAX_VALID_ID; ++i) { + if (bulletData.isVisible(i)) { + return true; + } + } + return false; } @Override diff --git a/src/main/java/bwapi/CommandTemp.java b/src/main/java/bwapi/CommandTemp.java index 11141319..d7836ffa 100644 --- a/src/main/java/bwapi/CommandTemp.java +++ b/src/main/java/bwapi/CommandTemp.java @@ -336,7 +336,7 @@ void execute(boolean isCurrentFrame) { // Shift training queue back one slot after the cancelled unit for (int i = command.extra; i < 4; ++i) { - unit.self().trainingQueue[i].set(unit.getTrainingQueue().get(i + 1), frame); + unit.self().trainingQueue[i].set(unit.getTrainingQueueAt(i + 1), frame); } unit.self().trainingQueueCount.setOrAdd(-1, frame); @@ -790,7 +790,7 @@ void execute(boolean isCurrentFrame) { unit.self().remainingUpgradeTime.set(upgradeType.upgradeTime(level + 1), frame); player.self().minerals.setOrAdd(-upgradeType.mineralPrice(level + 1), frame); - player.self().gas.setOrAdd(upgradeType.gasPrice(level + 1), frame); + player.self().gas.setOrAdd(-upgradeType.gasPrice(level + 1), frame); player.self().isUpgrading[upgradeType.id].set(true, frame); } diff --git a/src/main/java/bwapi/Game.java b/src/main/java/bwapi/Game.java index fda65689..6e853aee 100644 --- a/src/main/java/bwapi/Game.java +++ b/src/main/java/bwapi/Game.java @@ -162,6 +162,7 @@ void init() { forces[id] = new Force(gameData().getForces(id), id, this); } + // Note: C++ Client starts at 1, which is a legacy bug (The Server 0-indexes forces[0..3]). forceSet = Collections.unmodifiableList(Arrays.asList(forces)); final int playerCount = gameData().getPlayerCount(); @@ -270,21 +271,29 @@ void init() { mapPixelWidth = mapWidth * TilePosition.SIZE_IN_PIXELS; mapPixelHeight = mapHeight * TilePosition.SIZE_IN_PIXELS; - if (isReplay()) { + updatePlayers(); + setLatCom(!configuration.getAsync()); + } + + private void updatePlayers() { + if (isReplay() || self() == null) { enemies = Collections.emptyList(); allies = Collections.emptyList(); observers = Collections.emptyList(); + return; } - else { - enemies = playerSet.stream().filter(p -> !p.equals(self()) && self().isEnemy(p)) - .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); - allies = playerSet.stream().filter(p -> !p.equals(self()) && self().isAlly(p)) - .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); - - observers = playerSet.stream().filter(p -> !p.equals(self()) && p.isObserver()) - .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + final List newEnemies = new ArrayList<>(); + final List newAllies = new ArrayList<>(); + final List newObservers = new ArrayList<>(); + for (final Player p : getPlayers()) { + if (p.leftGame() || p.isDefeated() || p.equals(self())) continue; + if (self().isAlly(p)) newAllies.add(p); + if (self().isEnemy(p)) newEnemies.add(p); + if (p.isObserver()) newObservers.add(p); } - setLatCom(!configuration.getAsync()); + enemies = Collections.unmodifiableList(newEnemies); + allies = Collections.unmodifiableList(newAllies); + observers = Collections.unmodifiableList(newObservers); } void unitCreate(final int id) { @@ -317,6 +326,7 @@ void onFrame(final int frame) { .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); } getAllUnits().forEach(u -> u.updatePosition(frame)); + updatePlayers(); } /** @@ -577,7 +587,7 @@ public Position getMousePosition() { * and false if it was not. Returns false always if {@link Flag#UserInput} is disabled. * @see MouseButton */ - final public boolean getMouseState(final MouseButton button) { + public boolean getMouseState(final MouseButton button) { return gameData().getMouseState(button.id); } @@ -589,7 +599,7 @@ final public boolean getMouseState(final MouseButton button) { * and false if it was not. Returns false always if {@link Flag#UserInput} is disabled. * @see Key */ - final public boolean getKeyState(final Key key) { + public boolean getKeyState(final Key key) { return gameData().getKeyState(key.id); } @@ -645,7 +655,7 @@ public void pingMinimap(final Position p) { * @return true if the given flag is enabled, false if the flag is disabled. * @see Flag */ - final public boolean isFlagEnabled(final Flag flag) { + public boolean isFlagEnabled(final Flag flag) { return gameData().getFlags(flag.id); } @@ -878,11 +888,11 @@ public String mapHash() { * @param walkY The y coordinate of the mini-tile, in mini-tile units (8 pixels). * @return true if the mini-tile is walkable and false if it is impassable for ground units. */ - final public boolean isWalkable(final int walkX, final int walkY) { + public boolean isWalkable(final int walkX, final int walkY) { return isWalkable(new WalkPosition(walkX, walkY)); } - final public boolean isWalkable(final WalkPosition position) { + public boolean isWalkable(final WalkPosition position) { return position.isValid(this) && walkable[position.x][position.y]; } @@ -900,15 +910,15 @@ final public boolean isWalkable(final WalkPosition position) { * - 5: Very high ground doodad * . */ - final public int getGroundHeight(final int tileX, final int tileY) { + public int getGroundHeight(final int tileX, final int tileY) { return isValidTile(tileX, tileY) ? groundHeight[tileX][tileY] : 0; } - final public int getGroundHeight(final TilePosition position) { + public int getGroundHeight(final TilePosition position) { return getGroundHeight(position.x, position.y); } - final public boolean isBuildable(final int tileX, final int tileY) { + public boolean isBuildable(final int tileX, final int tileY) { return isBuildable(tileX, tileY, false); } @@ -924,11 +934,11 @@ final public boolean isBuildable(final int tileX, final int tileY) { * If includeBuildings was provided, then it will return false if a structure is currently * occupying the tile. */ - final public boolean isBuildable(final int tileX, final int tileY, final boolean includeBuildings) { + public boolean isBuildable(final int tileX, final int tileY, final boolean includeBuildings) { return isValidTile(tileX, tileY) && buildable[tileX][tileY] && (!includeBuildings || !gameData().isOccupied(tileX, tileY)); } - final public boolean isBuildable(final TilePosition position) { + public boolean isBuildable(final TilePosition position) { return isBuildable(position, false); } @@ -940,7 +950,7 @@ boolean isValidTile(final int x, final int y) { return x >= 0 && y >= 0 && x < mapWidth && y < mapHeight; } - final public boolean isBuildable(final TilePosition position, final boolean includeBuildings) { + public boolean isBuildable(final TilePosition position, final boolean includeBuildings) { return isBuildable(position.x, position.y, includeBuildings); } @@ -953,11 +963,11 @@ final public boolean isBuildable(final TilePosition position, final boolean incl * the value is true. If the given tile is concealed by the fog of war, then this value will * be false. */ - final public boolean isVisible(final int tileX, final int tileY) { + public boolean isVisible(final int tileX, final int tileY) { return isValidTile(tileX, tileY) && gameData().isVisible(tileX, tileY); } - final public boolean isVisible(final TilePosition position) { + public boolean isVisible(final TilePosition position) { return isVisible(position.x, position.y); } @@ -971,11 +981,11 @@ final public boolean isVisible(final TilePosition position) { * @return true if the player has explored the given tile position (partially revealed fog), false if the tile position was never explored (completely black fog). * @see #isVisible */ - final public boolean isExplored(final int tileX, final int tileY) { + public boolean isExplored(final int tileX, final int tileY) { return isValidTile(tileX, tileY) && gameData().isExplored(tileX, tileY); } - final public boolean isExplored(final TilePosition position) { + public boolean isExplored(final TilePosition position) { return isExplored(position.x, position.y); } @@ -986,15 +996,15 @@ final public boolean isExplored(final TilePosition position) { * @param tileY The y tile coordinate to check. * @return true if the given tile has creep on it, false if the given tile does not have creep, or if it is concealed by the fog of war. */ - final public boolean hasCreep(final int tileX, final int tileY) { + public boolean hasCreep(final int tileX, final int tileY) { return isValidTile(tileX, tileY) && gameData().getHasCreep(tileX, tileY); } - final public boolean hasCreep(final TilePosition position) { + public boolean hasCreep(final TilePosition position) { return hasCreep(position.x, position.y); } - final public boolean hasPowerPrecise(final int x, final int y) { + public boolean hasPowerPrecise(final int x, final int y) { return hasPowerPrecise(new Position(x, y)); } @@ -1007,38 +1017,38 @@ final public boolean hasPowerPrecise(final int x, final int y) { * @param unitType Checks if the given {@link UnitType} requires power or not. If ommitted, then it will assume that the position requires power for any unit type. * @return true if the type at the given position will have power, false if the type at the given position will be unpowered. */ - final public boolean hasPowerPrecise(final int x, final int y, final UnitType unitType) { + public boolean hasPowerPrecise(final int x, final int y, final UnitType unitType) { return isValidPosition(x, y) && hasPower(x, y, unitType, self().getUnits().stream().filter(u -> u.getType() == Protoss_Pylon).collect(Collectors.toList())); } - final public boolean hasPowerPrecise(final Position position) { + public boolean hasPowerPrecise(final Position position) { return hasPowerPrecise(position.x, position.y, UnitType.None); } - final public boolean hasPowerPrecise(final Position position, final UnitType unitType) { + public boolean hasPowerPrecise(final Position position, final UnitType unitType) { return hasPowerPrecise(position.x, position.y, unitType); } - final public boolean hasPower(final TilePosition position) { + public boolean hasPower(final TilePosition position) { return hasPower(position.x, position.y); } - final public boolean hasPower(final int tileX, final int tileY) { + public boolean hasPower(final int tileX, final int tileY) { return hasPower(tileX, tileY, UnitType.None); } - final public boolean hasPower(final TilePosition position, final UnitType unitType) { + public boolean hasPower(final TilePosition position, final UnitType unitType) { return hasPower(position.x, position.y, unitType); } - final public boolean hasPower(final int tileX, final int tileY, final UnitType unitType) { + public boolean hasPower(final int tileX, final int tileY, final UnitType unitType) { if (unitType.id >= 0 && unitType.id < UnitType.None.id) { return hasPowerPrecise(tileX * 32 + unitType.tileWidth() * 16, tileY * 32 + unitType.tileHeight() * 16, unitType); } return hasPowerPrecise(tileX * 32, tileY * 32, UnitType.None); } - final public boolean hasPower(final int tileX, final int tileY, final int tileWidth, final int tileHeight) { + public boolean hasPower(final int tileX, final int tileY, final int tileWidth, final int tileHeight) { return hasPower(tileX, tileY, tileWidth, tileHeight, UnitType.Unknown); } @@ -1051,23 +1061,23 @@ final public boolean hasPower(final int tileX, final int tileY, final int tileWi * @param unitType Checks if the given UnitType will be powered if placed at the given tile position. If omitted, then only the immediate tile position is checked for power, and the function will assume that the location requires power for any unit type. * @return true if the type at the given tile position will receive power, false if the type will be unpowered at the given tile position. */ - final public boolean hasPower(final int tileX, final int tileY, final int tileWidth, final int tileHeight, final UnitType unitType) { + public boolean hasPower(final int tileX, final int tileY, final int tileWidth, final int tileHeight, final UnitType unitType) { return hasPowerPrecise(tileX * 32 + tileWidth * 16, tileY * 32 + tileHeight * 16, unitType); } - final public boolean hasPower(final TilePosition position, final int tileWidth, final int tileHeight) { + public boolean hasPower(final TilePosition position, final int tileWidth, final int tileHeight) { return hasPower(position.x, position.y, tileWidth, tileHeight); } - final public boolean hasPower(final TilePosition position, final int tileWidth, final int tileHeight, final UnitType unitType) { + public boolean hasPower(final TilePosition position, final int tileWidth, final int tileHeight, final UnitType unitType) { return hasPower(position.x, position.y, tileWidth, tileHeight, unitType); } - final public boolean canBuildHere(final TilePosition position, final UnitType type, final Unit builder) { + public boolean canBuildHere(final TilePosition position, final UnitType type, final Unit builder) { return canBuildHere(position, type, builder, false); } - final public boolean canBuildHere(final TilePosition position, final UnitType type) { + public boolean canBuildHere(final TilePosition position, final UnitType type) { return canBuildHere(position, type, null); } @@ -1091,7 +1101,7 @@ final public boolean canBuildHere(final TilePosition position, final UnitType ty * @return true indicating that the structure can be placed at the given tile position, and * false if something may be obstructing the build location. */ - final public boolean canBuildHere(final TilePosition position, final UnitType type, final Unit builder, final boolean checkExplored) { + public boolean canBuildHere(final TilePosition position, final UnitType type, final Unit builder, final boolean checkExplored) { // lt = left top, rb = right bottom final TilePosition lt = builder != null && type.isAddon() ? position.add(new TilePosition(4, 1)) : // addon build offset @@ -1206,7 +1216,7 @@ final public boolean canBuildHere(final TilePosition position, final UnitType ty return true; } - final public boolean canMake(final UnitType type) { + public boolean canMake(final UnitType type) { return canMake(type, null); } @@ -1221,7 +1231,7 @@ final public boolean canMake(final UnitType type) { * only true if builder can make the type. Otherwise it will return false, indicating * that the unit type can not be made. */ - final public boolean canMake(final UnitType type, final Unit builder) { + public boolean canMake(final UnitType type, final Unit builder) { final Player pSelf = self(); // Error checking if (pSelf == null) { @@ -1331,11 +1341,11 @@ final public boolean canMake(final UnitType type, final Unit builder) { (builder.getAddon() != null && builder.getAddon().getType() == addon); } - final public boolean canResearch(final TechType type, final Unit unit) { + public boolean canResearch(final TechType type, final Unit unit) { return canResearch(type, unit, true); } - final public boolean canResearch(final TechType type) { + public boolean canResearch(final TechType type) { return canResearch(type, null); } @@ -1351,7 +1361,7 @@ final public boolean canResearch(final TechType type) { * only true if unit can research the type. Otherwise it will return false, indicating * that the technology can not be researched. */ - final public boolean canResearch(final TechType type, final Unit unit, final boolean checkCanIssueCommandType) { + public boolean canResearch(final TechType type, final Unit unit, final boolean checkCanIssueCommandType) { final Player self = self(); // Error checking if (self == null) { @@ -1395,11 +1405,11 @@ final public boolean canResearch(final TechType type, final Unit unit, final boo } - final public boolean canUpgrade(final UpgradeType type, final Unit unit) { + public boolean canUpgrade(final UpgradeType type, final Unit unit) { return canUpgrade(type, unit, true); } - final public boolean canUpgrade(final UpgradeType type) { + public boolean canUpgrade(final UpgradeType type) { return canUpgrade(type, null); } @@ -1415,7 +1425,7 @@ final public boolean canUpgrade(final UpgradeType type) { * only true if unit can upgrade the type. Otherwise it will return false, indicating * that the upgrade can not be upgraded. */ - final public boolean canUpgrade(final UpgradeType type, final Unit unit, final boolean checkCanIssueCommandType) { + public boolean canUpgrade(final UpgradeType type, final Unit unit, final boolean checkCanIssueCommandType) { final Player self = self(); if (self == null) { return false; @@ -1519,7 +1529,7 @@ public void sendTextEx(final boolean toAllies, final String string, final Text.. * * @return true if the client is in a game, and false if it is not. */ - final public boolean isInGame() { + public boolean isInGame() { return gameData().isInGame(); } @@ -1529,7 +1539,7 @@ final public boolean isInGame() { * @return true if the client is in a multiplayer game, and false if it is a single player * game, a replay, or some other state. */ - final public boolean isMultiplayer() { + public boolean isMultiplayer() { return multiplayer; } @@ -1539,7 +1549,7 @@ final public boolean isMultiplayer() { * * @return true if the client is in a multiplayer Battle.net game and false if it is not. */ - final public boolean isBattleNet() { + public boolean isBattleNet() { return battleNet; } @@ -1551,7 +1561,7 @@ final public boolean isBattleNet() { * @see #pauseGame * @see #resumeGame */ - final public boolean isPaused() { + public boolean isPaused() { return gameData().isPaused(); } @@ -1560,7 +1570,7 @@ final public boolean isPaused() { * * @return true if the client is watching a replay and false otherwise */ - final public boolean isReplay() { + public boolean isReplay() { return replay; } @@ -1633,7 +1643,7 @@ public void setLocalSpeed(final int speed) { * @return true if any one of the units in the List were capable of executing the * command, and false if none of the units were capable of executing the command. */ - final public boolean issueCommand(final Collection units, final UnitCommand command) { + public boolean issueCommand(final Collection units, final UnitCommand command) { return units.stream() .map(u -> u.issueCommand(command)) .reduce(false, (a, b) -> a | b); @@ -2053,15 +2063,15 @@ public void drawEllipseScreen(int x, int y, int xrad, int yrad, Color color) { } public void drawEllipseScreen(int x, int y, int xrad, int yrad, Color color, boolean isSolid) { - drawEllipse(CoordinateType.Mouse, x, y, xrad, yrad, color, isSolid); + drawEllipse(CoordinateType.Screen, x, y, xrad, yrad, color, isSolid); } public void drawEllipseScreen(Position p, int xrad, int yrad, Color color) { - drawEllipse(CoordinateType.Mouse, p.x, p.y, xrad, yrad, color); + drawEllipse(CoordinateType.Screen, p.x, p.y, xrad, yrad, color); } public void drawEllipseScreen(Position p, int xrad, int yrad, Color color, boolean isSolid) { - drawEllipse(CoordinateType.Mouse, p.x, p.y, xrad, yrad, color, isSolid); + drawEllipse(CoordinateType.Screen, p.x, p.y, xrad, yrad, color, isSolid); } /** @@ -2205,7 +2215,7 @@ public int getRevision() { * * @return true if the BWAPI module is a DEBUG build, and false if it is a RELEASE build. */ - final public boolean isDebug() { + public boolean isDebug() { return debug; } @@ -2215,7 +2225,7 @@ final public boolean isDebug() { * @return true if latency compensation is enabled, false if it is disabled. * @see #setLatCom */ - final public boolean isLatComEnabled() { + public boolean isLatComEnabled() { return latcom; } @@ -2287,7 +2297,7 @@ public void setFrameSkip(int frameSkip) { * allied players have eliminated their opponents. Otherwise, the game will only end if * no other players are remaining in the game. This value is true by default. */ - final public boolean setAlliance(Player player, boolean allied, boolean alliedVictory) { + public boolean setAlliance(Player player, boolean allied, boolean alliedVictory) { if (self() == null || isReplay() || player == null || player.equals(self())) { return false; } @@ -2296,11 +2306,11 @@ final public boolean setAlliance(Player player, boolean allied, boolean alliedVi return true; } - final public boolean setAlliance(Player player, boolean allied) { + public boolean setAlliance(Player player, boolean allied) { return setAlliance(player, allied, true); } - final public boolean setAlliance(Player player) { + public boolean setAlliance(Player player) { return setAlliance(player, true); } @@ -2316,7 +2326,7 @@ final public boolean setAlliance(Player player) { * of the target player will be shown, otherwise the target player will be hidden. This * value is true by default. */ - final public boolean setVision(Player player, boolean enabled) { + public boolean setVision(Player player, boolean enabled) { if (player == null) { return false; } @@ -2381,7 +2391,7 @@ public int getLastEventTime() { * does not have permission from the tournament module, failed to find the map specified, or received an invalid * parameter. */ - final public boolean setMap(final String mapFileName) { + public boolean setMap(final String mapFileName) { if (mapFileName == null || mapFileName.isEmpty() || mapFileName.length() >= 260 || mapFileName.charAt(0) == 0) { return false; } @@ -2395,7 +2405,7 @@ final public boolean setMap(final String mapFileName) { * @param reveal The state of the reveal all flag. If false, all fog of war will be enabled. If true, * then the fog of war will be revealed. It is true by default. */ - final public boolean setRevealAll(boolean reveal) { + public boolean setRevealAll(boolean reveal) { if (!isReplay()) { return false; } @@ -2403,7 +2413,7 @@ final public boolean setRevealAll(boolean reveal) { return true; } - final public boolean setRevealAll() { + public boolean setRevealAll() { return setRevealAll(true); } @@ -2423,7 +2433,7 @@ final public boolean setRevealAll() { * @return true if there is a path between the two positions, and false if there is not. * @see Unit#hasPath */ - final public boolean hasPath(final Position source, final Position destination) { + public boolean hasPath(final Position source, final Position destination) { if (source == null || destination == null) { return false; } diff --git a/src/main/java/bwapi/Player.java b/src/main/java/bwapi/Player.java index 16e932a3..e2d8710b 100644 --- a/src/main/java/bwapi/Player.java +++ b/src/main/java/bwapi/Player.java @@ -19,6 +19,8 @@ * @see Race */ public class Player implements Comparable { + static final int MAX_VALID_ID = 9; + private final PlayerData playerData; private final Game game; private final int id; @@ -53,6 +55,10 @@ public int getID() { return id; } + boolean hasValidId() { + return id >= 0 && id < MAX_VALID_ID; + } + /** * Retrieves the name of the player. * @@ -141,7 +147,7 @@ public boolean isEnemy(final Player player) { if (player == null || isNeutral() || player.isNeutral() || isObserver() || player.isObserver()) { return false; } - return !playerData.isAlly(player.getID()); + return playerData.isEnemy(player.getID()); } /** @@ -150,7 +156,7 @@ public boolean isEnemy(final Player player) { * @return true if this player is the neutral player, false if this player is any other player. */ public boolean isNeutral() { - return equals(game.neutral()); + return playerData.isNeutral(); } /** @@ -339,8 +345,9 @@ public int supplyTotal() { * @return The total supply available for this player and the given race. * @see #supplyUsed */ - public int supplyTotal(final Race race) { - return playerData.getSupplyTotal(race.id); + public int supplyTotal(Race race) { + race = race == Race.None ? getRace() : race; + return race.hasValidId() ? playerData.getSupplyTotal(race.id) : 0; } public int supplyUsed() { @@ -354,7 +361,11 @@ public int supplyUsed() { * @return The supply that is in use for this player and the given race. * @see #supplyTotal */ - public int supplyUsed(final Race race) { + public int supplyUsed(Race race) { + race = race == Race.None ? getRace() : race; + if (!race.hasValidId()) { + return 0; + } int supplyUsed = playerData.getSupplyUsed(race.id); if (game.isLatComEnabled() && self().supplyUsed[race.id].valid(game.getFrameCount())) { return supplyUsed + self().supplyUsed[race.id].get(); @@ -544,6 +555,8 @@ public Color getColor() { * @return character code to use for text in Broodwar. */ public Text getTextColor() { + // Note: C++ BWAPI 4.4.0 uses Colors::White (255) and Colors::Green (117) due to a bug, + // whereas Player 7 white is 84 (DirtyWhite) and Player 9 / Ice green is 185 (DarkGreen). switch (getColor().id) { case 111: // red return Text.BrightRed; diff --git a/src/main/java/bwapi/Race.java b/src/main/java/bwapi/Race.java index a3580276..19e6d3e3 100644 --- a/src/main/java/bwapi/Race.java +++ b/src/main/java/bwapi/Race.java @@ -58,12 +58,18 @@ public enum Race { Arrays.stream(Race.values()).forEach(v -> idToEnum[v.id] = v); } + static final int MAX_VALID_ID = 3; + public final int id; Race(final int id) { this.id = id; } + boolean hasValidId() { + return id >= 0 && id < MAX_VALID_ID; + } + /** * Retrieves the default worker type for this {@link Race}. *

diff --git a/src/main/java/bwapi/Region.java b/src/main/java/bwapi/Region.java index 57f228b9..25bbb83f 100644 --- a/src/main/java/bwapi/Region.java +++ b/src/main/java/bwapi/Region.java @@ -64,7 +64,7 @@ void updateNeighbours() { final Region region = game.getRegion(regionData.getNeighbors(i)); neighbours.add(region); final int d = getDistance(region); - if (region.isAccessible()) { + if (this.getRegionGroupID() == region.getRegionGroupID()) { if (d < accessibleBestDist) { closestAccessibleRegion = region; accessibleBestDist = d; diff --git a/src/main/java/bwapi/Unit.java b/src/main/java/bwapi/Unit.java index 5e51a48a..53a3a0da 100644 --- a/src/main/java/bwapi/Unit.java +++ b/src/main/java/bwapi/Unit.java @@ -2092,8 +2092,17 @@ public boolean isVisible() { * fog of war will be accessible, but isVisible will still return false. * @see #exists */ - public boolean isVisible(final Player player) { - return unitData.isVisible(player.getID()); + public boolean isVisible(Player player) { + player = player == null ? game.self() : player; + if (player != null) { + return player.hasValidId() && unitData.isVisible(player.getID()); + } + for (int i = 0; i < Player.MAX_VALID_ID; ++i) { + if (unitData.isVisible(i)) { + return true; + } + } + return false; } /** @@ -2107,6 +2116,9 @@ public boolean isTargetable() { if (!exists()) { return false; } + if (!isVisible() && !game.isFlagEnabled(Flag.CompleteMapInformation)) { + return false; + } final UnitType ut = getType(); if (!isCompleted() && !ut.isBuilding() && @@ -2205,7 +2217,7 @@ public boolean attack(final Unit target, final boolean shiftQueueCommand) { } public boolean build(final UnitType type) { - return issueCommand(UnitCommand.train(this, type)); + return build(type, TilePosition.None); } /** @@ -2225,6 +2237,9 @@ public boolean build(final UnitType type) { * @see Unit#canBuild */ public boolean build(final UnitType type, final TilePosition target) { + if (TilePosition.None.equals(target)) { + return train(type); + } return issueCommand(UnitCommand.build(this, target, type)); } @@ -2269,7 +2284,23 @@ public boolean buildAddon(final UnitType type) { * @see Unit#isTraining * @see Unit#canTrain */ - public boolean train(final UnitType type) { + public boolean train() { + return train(UnitType.None); + } + + public boolean train(UnitType type) { + if (type == UnitType.None) { + switch (getType()) { + case Protoss_Carrier: + case Hero_Gantrithor: + type = Protoss_Interceptor; + break; + case Protoss_Reaver: + case Hero_Warbringer: + type = Protoss_Scarab; + break; + } + } return issueCommand(UnitCommand.train(this, type)); } @@ -2876,6 +2907,9 @@ public boolean useTech(final TechType tech, final Position target) { } public boolean useTech(final TechType tech, final Unit target) { + if (target == null) { + return issueCommand(UnitCommand.useTech(this, tech)); + } return issueCommand(UnitCommand.useTech(this, tech, target)); } @@ -3506,18 +3540,7 @@ public boolean canIssueCommandTypeGrouped(UnitCommandType ct, boolean checkComma } public boolean canTargetUnit(Unit targetUnit) { - if (targetUnit == null || !targetUnit.exists()) { - return false; - } - final UnitType targetType = targetUnit.getType(); - if (!targetUnit.isCompleted() && !targetType.isBuilding() && !targetUnit.isMorphing() && - targetType != Protoss_Archon && targetType != Protoss_Dark_Archon) { - return false; - } - return targetType != Spell_Scanner_Sweep && - targetType != Spell_Dark_Swarm && - targetType != Spell_Disruption_Web && - targetType != Special_Map_Revealer; + return canTargetUnit(targetUnit, true); } /** @@ -3534,7 +3557,7 @@ public boolean canTargetUnit(Unit targetUnit, boolean checkCommandibility) { if (checkCommandibility && !canCommand()) { return false; } - return canTargetUnit(targetUnit); + return targetUnit != null && targetUnit.isTargetable(); } public boolean canAttack() { @@ -3668,7 +3691,7 @@ public boolean canAttackGrouped(Position target, boolean checkCanTargetUnit, boo if (checkCommandibilityGrouped && !canCommandGrouped(false)) { return false; } - return canAttackMoveGrouped(false, false) || canAttackUnitGrouped(false, false); + return canAttackMoveGrouped(false, false); } public boolean canAttackGrouped(Unit target, boolean checkCanTargetUnit, boolean checkCanIssueCommandType, boolean checkCommandibilityGrouped, boolean checkCommandibility) { @@ -4051,7 +4074,7 @@ public boolean canBuildAddon(UnitType uType, boolean checkCanIssueCommandType, b if (checkCommandibility && !canCommand()) { return false; } - if (checkCanIssueCommandType && !canBuildAddon(uType, false)) { + if (checkCanIssueCommandType && !canBuildAddon(false)) { return false; } if (!game.canMake(uType, this)) { diff --git a/src/main/java/bwapi/UnitCommand.java b/src/main/java/bwapi/UnitCommand.java index 4752d231..d67d3fb8 100644 --- a/src/main/java/bwapi/UnitCommand.java +++ b/src/main/java/bwapi/UnitCommand.java @@ -20,6 +20,7 @@ private UnitCommand(final Unit unit, final UnitCommandType type) { } private > void assignTarget(Point target) { + // Note: C++ BWAPI clamps target via target.makeValid() to [0, mapWidth] and [0, mapHeight]. this.x = target.x; this.y = target.y; } @@ -401,6 +402,7 @@ public boolean isQueued() { type == Follow || type == Gather || type == Return_Cargo || + type == Repair || type == Load || type == Unload_All || type == Unload_All_Position || @@ -409,6 +411,7 @@ public boolean isQueued() { && extra != 0; } + // Note: JBWAPI includes `unit` in equals and hashCode, C++ BWAPI does not. @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/test/java/bwapi/BulletTest.java b/src/test/java/bwapi/BulletTest.java new file mode 100644 index 00000000..60c47ad9 --- /dev/null +++ b/src/test/java/bwapi/BulletTest.java @@ -0,0 +1,32 @@ +package bwapi; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BulletTest { + + @Test + public void isVisibleShouldFallbackToAnyPlayerWhenSelfIsNullAndCheckValidPlayerId() { + Game game = mock(Game.class); + when(game.self()).thenReturn(null); + + ClientData.BulletData bulletData = mock(ClientData.BulletData.class); + Bullet bullet = new Bullet(bulletData, 0, game); + + assertFalse(bullet.isVisible()); + + when(bulletData.isVisible(2)).thenReturn(true); + assertTrue(bullet.isVisible()); + assertTrue(bullet.isVisible(null)); + + ClientData.PlayerData neutralData = mock(ClientData.PlayerData.class); + when(neutralData.getName()).thenReturn("Neutral"); + Player neutral = new Player(neutralData, 11, game); + when(bulletData.isVisible(11)).thenReturn(true); + assertFalse(bullet.isVisible(neutral)); + } +} diff --git a/src/test/java/bwapi/CommandTempTest.java b/src/test/java/bwapi/CommandTempTest.java new file mode 100644 index 00000000..9af8b90b --- /dev/null +++ b/src/test/java/bwapi/CommandTempTest.java @@ -0,0 +1,64 @@ +package bwapi; + +import org.junit.Test; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class CommandTempTest { + + @Test + public void upgradeShouldSubtractGasInsteadOfAdding() { + Game game = spy(new Game()); + doReturn(10).when(game).getFrameCount(); + + ClientData.PlayerData playerData = mock(ClientData.PlayerData.class); + when(playerData.getName()).thenReturn("Self"); + when(playerData.getMinerals()).thenReturn(500); + when(playerData.getGas()).thenReturn(500); + Player player = new Player(playerData, 0, game); + + Unit unit = spy(new Unit(mock(ClientData.UnitData.class), 0, game)); + doReturn(true).when(unit).exists(); + doReturn(player).when(unit).getPlayer(); + + UpgradeType upgrade = UpgradeType.Terran_Infantry_Weapons; + CommandTemp cmd = new CommandTemp(UnitCommand.upgrade(unit, upgrade), game); + cmd.execute(false); + + assertEquals(500 - upgrade.mineralPrice(1), player.minerals()); + assertEquals(500 - upgrade.gasPrice(1), player.gas()); + } + + @Test + public void cancelTrainSlotShouldShiftQueueWithoutIndexOutOfBounds() { + Game game = spy(new Game()); + doReturn(10).when(game).getFrameCount(); + + ClientData.PlayerData playerData = mock(ClientData.PlayerData.class); + when(playerData.getName()).thenReturn("Self"); + Player player = new Player(playerData, 0, game); + + ClientData.UnitData unitData = mock(ClientData.UnitData.class); + when(unitData.getExists()).thenReturn(true); + when(unitData.getTrainingQueueCount()).thenReturn(2); + when(unitData.getTrainingQueue(0)).thenReturn(UnitType.Terran_Marine.id); + when(unitData.getTrainingQueue(1)).thenReturn(UnitType.Terran_Medic.id); + when(unitData.getTrainingQueue(2)).thenReturn(UnitType.None.id); + when(unitData.getTrainingQueue(3)).thenReturn(UnitType.None.id); + when(unitData.getTrainingQueue(4)).thenReturn(UnitType.None.id); + + Unit unit = spy(new Unit(unitData, 0, game)); + doReturn(player).when(unit).getPlayer(); + + CommandTemp cmd = new CommandTemp(UnitCommand.cancelTrain(unit, 1), game); + cmd.execute(false); + + assertEquals(Collections.singletonList(UnitType.Terran_Marine), unit.getTrainingQueue()); + } +} diff --git a/src/test/java/bwapi/GameTest.java b/src/test/java/bwapi/GameTest.java index 396d2c81..f2fdb52a 100644 --- a/src/test/java/bwapi/GameTest.java +++ b/src/test/java/bwapi/GameTest.java @@ -9,6 +9,7 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -174,4 +175,39 @@ public void setMapWithEmptyStringShouldReturnFalseInsteadOfThrowing() { Game game = new Game(); assertFalse(game.setMap("")); } + + @Test + public void onFrameShouldRefreshPlayersAndExcludeDefeatedOrLeftPlayers() { + Game game = spy(new Game()); + Player self = mock(Player.class); + Player ally = mock(Player.class); + Player enemy = mock(Player.class); + Player defeatedEnemy = mock(Player.class); + Player leftAlly = mock(Player.class); + Player observer = mock(Player.class); + + when(self.isAlly(ally)).thenReturn(true); + when(self.isAlly(leftAlly)).thenReturn(true); + when(self.isEnemy(enemy)).thenReturn(true); + when(self.isEnemy(defeatedEnemy)).thenReturn(true); + + when(defeatedEnemy.isDefeated()).thenReturn(true); + when(leftAlly.leftGame()).thenReturn(true); + when(observer.isObserver()).thenReturn(true); + + doReturn(self).when(game).self(); + doReturn(Arrays.asList(self, ally, enemy, defeatedEnemy, leftAlly, observer)).when(game).getPlayers(); + game.setAllUnits(Collections.emptyList()); + + game.onFrame(0); + + assertEquals(Collections.singletonList(ally), game.allies()); + assertEquals(Collections.singletonList(enemy), game.enemies()); + assertEquals(Collections.singletonList(observer), game.observers()); + + when(enemy.isDefeated()).thenReturn(true); + game.onFrame(1); + + assertTrue(game.enemies().isEmpty()); + } } \ No newline at end of file diff --git a/src/test/java/bwapi/PlayerTest.java b/src/test/java/bwapi/PlayerTest.java index 83da83db..33d73796 100644 --- a/src/test/java/bwapi/PlayerTest.java +++ b/src/test/java/bwapi/PlayerTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -28,4 +29,46 @@ public void getTextColorShouldReturnDarkGreenForColorGreen() { assertEquals(Text.DarkGreen, player.getTextColor()); } + + @Test + public void isEnemyAndIsNeutralShouldDelegateToPlayerData() { + Game game = mock(Game.class); + ClientData.PlayerData selfData = mock(ClientData.PlayerData.class); + ClientData.PlayerData otherData = mock(ClientData.PlayerData.class); + when(selfData.getName()).thenReturn("Self"); + when(otherData.getName()).thenReturn("Other"); + when(selfData.isParticipating()).thenReturn(true); + when(otherData.isParticipating()).thenReturn(true); + // Neither ally nor enemy in PlayerData: + when(selfData.isAlly(1)).thenReturn(false); + when(selfData.isEnemy(1)).thenReturn(false); + + Player self = new Player(selfData, 0, game); + Player other = new Player(otherData, 1, game); + + assertFalse(self.isEnemy(other)); + + when(selfData.isEnemy(1)).thenReturn(true); + assertTrue(self.isEnemy(other)); + + when(otherData.isNeutral()).thenReturn(true); + assertTrue(other.isNeutral()); + } + + @Test + public void supplyTotalAndSupplyUsedShouldHandleRaceNoneAndInvalidRace() { + Game game = mock(Game.class); + ClientData.PlayerData playerData = mock(ClientData.PlayerData.class); + when(playerData.getName()).thenReturn("Self"); + when(playerData.getRace()).thenReturn(Race.Terran.id); + when(playerData.getSupplyTotal(Race.Terran.id)).thenReturn(40); + when(playerData.getSupplyUsed(Race.Terran.id)).thenReturn(24); + + Player player = new Player(playerData, 0, game); + + assertEquals(40, player.supplyTotal(Race.None)); + assertEquals(24, player.supplyUsed(Race.None)); + assertEquals(0, player.supplyTotal(Race.Unknown)); + assertEquals(0, player.supplyUsed(Race.Unknown)); + } } diff --git a/src/test/java/bwapi/RegionTest.java b/src/test/java/bwapi/RegionTest.java new file mode 100644 index 00000000..d2c459e9 --- /dev/null +++ b/src/test/java/bwapi/RegionTest.java @@ -0,0 +1,53 @@ +package bwapi; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RegionTest { + + @Test + public void updateNeighboursShouldDetermineAccessibilityByComparingRegionGroupId() { + Game game = mock(Game.class); + + ClientData.RegionData originData = mock(ClientData.RegionData.class); + when(originData.getId()).thenReturn(0); + when(originData.islandID()).thenReturn(10); + when(originData.getCenter_x()).thenReturn(100); + when(originData.getCenter_y()).thenReturn(100); + when(originData.isAccessible()).thenReturn(true); + when(originData.getNeighborCount()).thenReturn(2); + when(originData.getNeighbors(0)).thenReturn(1); + when(originData.getNeighbors(1)).thenReturn(2); + + // Neighbor 1 is closer and walkable (isAccessible == true), but on a different island/group + ClientData.RegionData diffGroupData = mock(ClientData.RegionData.class); + when(diffGroupData.getId()).thenReturn(1); + when(diffGroupData.islandID()).thenReturn(20); + when(diffGroupData.getCenter_x()).thenReturn(120); + when(diffGroupData.getCenter_y()).thenReturn(100); + when(diffGroupData.isAccessible()).thenReturn(true); + + // Neighbor 2 is farther, but in the same region group + ClientData.RegionData sameGroupData = mock(ClientData.RegionData.class); + when(sameGroupData.getId()).thenReturn(2); + when(sameGroupData.islandID()).thenReturn(10); + when(sameGroupData.getCenter_x()).thenReturn(200); + when(sameGroupData.getCenter_y()).thenReturn(100); + when(sameGroupData.isAccessible()).thenReturn(true); + + Region origin = new Region(originData, game); + Region diffGroupNeighbor = new Region(diffGroupData, game); + Region sameGroupNeighbor = new Region(sameGroupData, game); + + when(game.getRegion(1)).thenReturn(diffGroupNeighbor); + when(game.getRegion(2)).thenReturn(sameGroupNeighbor); + + origin.updateNeighbours(); + + assertEquals(sameGroupNeighbor, origin.getClosestAccessibleRegion()); + assertEquals(diffGroupNeighbor, origin.getClosestInaccessibleRegion()); + } +} diff --git a/src/test/java/bwapi/UnitTest.java b/src/test/java/bwapi/UnitTest.java index dc31a7e3..1e404b07 100644 --- a/src/test/java/bwapi/UnitTest.java +++ b/src/test/java/bwapi/UnitTest.java @@ -10,10 +10,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -269,4 +271,113 @@ public void canUseTechUnitHealingShouldRejectEnemyTarget() { assertFalse(medic.canUseTechUnit(TechType.Healing, enemyMarine, false, false, false, false)); } + + @Test + public void isVisibleShouldFallbackToAnyPlayerWhenSelfIsNullAndCheckValidPlayerId() { + Game game = mock(Game.class); + when(game.self()).thenReturn(null); + + ClientData.UnitData unitData = mock(ClientData.UnitData.class); + Unit unit = new Unit(unitData, 0, game); + + assertFalse(unit.isVisible()); + + when(unitData.isVisible(3)).thenReturn(true); + assertTrue(unit.isVisible()); + assertTrue(unit.isVisible(null)); + + ClientData.PlayerData neutralData = mock(ClientData.PlayerData.class); + when(neutralData.getName()).thenReturn("Neutral"); + Player neutral = new Player(neutralData, 11, game); + when(unitData.isVisible(11)).thenReturn(true); + assertFalse(unit.isVisible(neutral)); + } + + @Test + public void isTargetableShouldRequireVisibilityWhenCompleteMapInfoDisabled() { + Game game = mock(Game.class); + Unit unit = spy(new Unit(mock(ClientData.UnitData.class), 0, game)); + doReturn(true).when(unit).exists(); + doReturn(false).when(unit).isVisible(); + doReturn(true).when(unit).isCompleted(); + doReturn(UnitType.Terran_Marine).when(unit).getType(); + + when(game.isFlagEnabled(Flag.CompleteMapInformation)).thenReturn(false); + assertFalse(unit.isTargetable()); + + when(game.isFlagEnabled(Flag.CompleteMapInformation)).thenReturn(true); + assertTrue(unit.isTargetable()); + } + + @Test + public void canTargetUnitShouldCheckCommandabilityByDefaultAndDelegateToIsTargetable() { + Game game = mock(Game.class); + Unit source = spy(new Unit(mock(ClientData.UnitData.class), 0, game)); + Unit target = mock(Unit.class); + when(target.exists()).thenReturn(true); + when(target.isCompleted()).thenReturn(true); + when(target.getType()).thenReturn(UnitType.Terran_Marine); + when(target.isTargetable()).thenReturn(true); + + doReturn(false).when(source).canCommand(); + assertFalse(source.canTargetUnit(target)); + assertTrue(source.canTargetUnit(target, false)); + + doReturn(true).when(source).canCommand(); + when(target.isTargetable()).thenReturn(false); + assertFalse(source.canTargetUnit(target)); + } + + @Test + public void buildWithNonePositionAndTrainWithNoneUnitTypeShouldRedirectToCarrierAndReaverAmmo() { + Game game = mock(Game.class); + Unit carrier = spy(new Unit(mock(ClientData.UnitData.class), 0, game)); + doReturn(UnitType.Protoss_Carrier).when(carrier).getType(); + doReturn(true).when(carrier).issueCommand(any(UnitCommand.class)); + + assertTrue(carrier.build(UnitType.None, TilePosition.None)); + verify(carrier).issueCommand(UnitCommand.train(carrier, UnitType.Protoss_Interceptor)); + + Unit reaver = spy(new Unit(mock(ClientData.UnitData.class), 1, game)); + doReturn(UnitType.Protoss_Reaver).when(reaver).getType(); + doReturn(true).when(reaver).issueCommand(any(UnitCommand.class)); + + assertTrue(reaver.build(UnitType.None)); + verify(reaver).issueCommand(UnitCommand.train(reaver, UnitType.Protoss_Scarab)); + } + + @Test + public void useTechWithNullUnitTargetShouldIssueTargetlessUseTechCommand() { + Game game = mock(Game.class); + Unit marine = spy(new Unit(mock(ClientData.UnitData.class), 0, game)); + doReturn(true).when(marine).issueCommand(any(UnitCommand.class)); + + assertTrue(marine.useTech(TechType.Stim_Packs, (Unit) null)); + verify(marine).issueCommand(UnitCommand.useTech(marine, TechType.Stim_Packs)); + } + + @Test + public void canAttackGroupedPositionShouldOnlyCheckCanAttackMoveGrouped() { + Game game = mock(Game.class); + Unit unit = spy(new Unit(mock(ClientData.UnitData.class), 0, game)); + doReturn(false).when(unit).canAttackMoveGrouped(false, false); + doReturn(true).when(unit).canAttackUnitGrouped(false, false); + + assertFalse(unit.canAttackGrouped(new Position(100, 100), false, true, false, false)); + } + + @Test + public void canBuildAddonWithUnitTypeShouldCheckCanBuildAddonWithoutRecursingOnUnitType() { + Game game = mock(Game.class); + when(game.canMake(any(UnitType.class), any(Unit.class))).thenReturn(true); + when(game.canBuildHere(any(TilePosition.class), any(UnitType.class), any(Unit.class))).thenReturn(true); + + Unit factory = spy(new Unit(mock(ClientData.UnitData.class), 0, game)); + doReturn(UnitType.Terran_Factory).when(factory).getType(); + doReturn(new TilePosition(10, 10)).when(factory).getTilePosition(); + // Not completed -> canBuildAddon(false) is false + doReturn(false).when(factory).isCompleted(); + + assertFalse(factory.canBuildAddon(UnitType.Terran_Machine_Shop, true, false)); + } }