diff --git a/client/src/components/sidebar/map-editor/MapGenerator.ts b/client/src/components/sidebar/map-editor/MapGenerator.ts index 5fcdb3c1..dcaa1ca9 100644 --- a/client/src/components/sidebar/map-editor/MapGenerator.ts +++ b/client/src/components/sidebar/map-editor/MapGenerator.ts @@ -4,9 +4,10 @@ import Match from '../../../playback/Match' import { CurrentMap, StaticMap } from '../../../playback/Map' import Round from '../../../playback/Round' import Bodies from '../../../playback/Bodies' -import { BATTLECODE_YEAR, DIRECTIONS } from '../../../constants' +import { BATTLECODE_YEAR, DIRECTIONS, TEAM_COLOR_NAMES } from '../../../constants' import { nativeAPI } from '../runner/native-api-wrapper' import { Vector } from '../../../playback/Vector' +import { RobotType } from 'battlecode-schema/js/battlecode/schema' export function loadFileAsMap(file: File): Promise { return new Promise((resolve, reject) => { @@ -54,8 +55,6 @@ function verifyMap(map: CurrentMap, bodies: Bodies): string { // Validate map elements let numWalls = 0 - let numPaintTowers = 0 - let numMoneyTowers = 0 const mapSize = map.width * map.height for (let i = 0; i < mapSize; i++) { const pos = map.indexToLocation(i) @@ -104,8 +103,6 @@ function verifyMap(map: CurrentMap, bodies: Bodies): string { } } - numPaintTowers += body && body.robotType === schema.RobotType.PAINT_TOWER ? 1 : 0 - numMoneyTowers += body && body.robotType === schema.RobotType.MONEY_TOWER ? 1 : 0 numWalls += wall } @@ -117,24 +114,61 @@ function verifyMap(map: CurrentMap, bodies: Bodies): string { } // Validate initial bodies - if (numPaintTowers !== 2) { - return `Expected exactly 2 paint towers, found ${numPaintTowers}` - } - if (numMoneyTowers !== 2) { - return `Expected exactly 2 money towers, found ${numMoneyTowers}` - } + const numPaintTowers = [0, 0] + const numMoneyTowers = [0, 0] for (const body of bodies.bodies.values()) { - // Check distance to nearby ruins + // Check distance to nearby ruins, towers, and walls + + if (body.robotType === RobotType.PAINT_TOWER) { + numPaintTowers[body.team.id - 1]++ + } else if (body.robotType === RobotType.MONEY_TOWER) { + numMoneyTowers[body.team.id - 1]++ + } else { + return `Tower at (${body.pos.x}, ${body.pos.y}) has invalid type!` + } for (const checkRuin of map.staticMap.ruins) { - if (squareIntersects(checkRuin, body.pos, 2)) { + if (squareIntersects(checkRuin, body.pos, 4)) { return ( `Tower at (${body.pos.x}, ${body.pos.y}) is too close to ruin ` + `at (${checkRuin.x}, ${checkRuin.y}), must be ` + - `>= 3 away` + `>= 5 away` + ) + } + } + + for (const checkBody of bodies.bodies.values()) { + if (checkBody === body) continue + if (squareIntersects(checkBody.pos, body.pos, 4)) { + return ( + `Tower at (${body.pos.x}, ${body.pos.y}) is too close to ruin ` + + `at (${checkBody.pos.x}, ${checkBody.pos.y}), must be ` + + `>= 5 away` ) } } + + const wall = map.staticMap.walls.findIndex( + (v, i) => !!v && squareIntersects(map.indexToLocation(i), body.pos, 2) + ) + if (wall !== -1) { + const pos = map.indexToLocation(wall) + return ( + `Tower at (${body.pos.x}, ${body.pos.y}) is too close to wall ` + + `at (${pos.x}, ${pos.y}), must be ` + + `>= 3 away` + ) + } + } + + for (const teamIdx of [0, 1]) { + if (numPaintTowers[teamIdx] !== 2) { + return `Expected exactly 2 ${TEAM_COLOR_NAMES[teamIdx]} paint towers, found ${numPaintTowers[teamIdx]}` + } + + if (numMoneyTowers[teamIdx] !== 2) { + return `Expected exactly 2 ${TEAM_COLOR_NAMES[teamIdx]} money towers, found ${numMoneyTowers[teamIdx]}` + } } return '' diff --git a/client/src/components/sidebar/map-editor/map-editor.tsx b/client/src/components/sidebar/map-editor/map-editor.tsx index 83eae77b..1e55fbaf 100644 --- a/client/src/components/sidebar/map-editor/map-editor.tsx +++ b/client/src/components/sidebar/map-editor/map-editor.tsx @@ -160,7 +160,7 @@ export const MapEditorPage: React.FC = (props) => { GameRunner.setMatch(editGame.current.currentMatch) const round = editGame.current.currentMatch!.currentRound - const brushes = round.map.getEditorBrushes().concat(round.bodies.getEditorBrushes(round.map.staticMap)) + const brushes = round.map.getEditorBrushes(round).concat(round.bodies.getEditorBrushes(round)) brushes[0].open = true setBrushes(brushes) setCleared(round.bodies.isEmpty() && round.map.isEmpty()) diff --git a/client/src/constants.ts b/client/src/constants.ts index a88c9c26..1e109962 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -1,4 +1,4 @@ -export const CLIENT_VERSION = '1.0.0' +export const CLIENT_VERSION = '1.1.0' export const SPEC_VERSION = '1' export const BATTLECODE_YEAR: number = 2025 export const MAP_SIZE_RANGE = { diff --git a/client/src/playback/Bodies.ts b/client/src/playback/Bodies.ts index fcb18878..0729e88d 100644 --- a/client/src/playback/Bodies.ts +++ b/client/src/playback/Bodies.ts @@ -187,8 +187,8 @@ export default class Bodies { return this.bodies.size === 0 } - getEditorBrushes(map: StaticMap): MapEditorBrush[] { - return [new TowerBrush(this, map)] + getEditorBrushes(round: Round): MapEditorBrush[] { + return [new TowerBrush(round)] } toInitialBodyTable(builder: flatbuffers.Builder): number { @@ -381,6 +381,15 @@ export class Body { const squares2 = this.getAllLocationsWithinRadiusSquared(match, pos, this.metadata.visionRadiusSquared()) this.drawEdges(match, ctx, lightly, squares2) + // Currently vision/message radius are always the same + /* + ctx.beginPath() + ctx.strokeStyle = 'brown' + ctx.lineWidth = 0.1 + const squares3 = this.getAllLocationsWithinRadiusSquared(match, pos, this.metadata.messageRadiusSquared()) + this.drawEdges(match, ctx, lightly, squares3) + */ + ctx.globalAlpha = 1 } diff --git a/client/src/playback/Brushes.ts b/client/src/playback/Brushes.ts index 4d884149..132361de 100644 --- a/client/src/playback/Brushes.ts +++ b/client/src/playback/Brushes.ts @@ -9,6 +9,7 @@ import Bodies from './Bodies' import { CurrentMap, StaticMap } from './Map' import { Vector } from './Vector' import { Team } from './Game' +import Round from './Round' const applyInRadius = ( map: CurrentMap | StaticMap, @@ -40,7 +41,35 @@ const squareIntersects = (check: Vector, center: Vector, radius: number) => { ) } +const checkValidRuinPlacement = (check: Vector, map: StaticMap, bodies: Bodies) => { + // Check if ruin is too close to the border + if (check.x <= 1 || check.x >= map.width - 2 || check.y <= 1 || check.y >= map.height - 2) { + return false + } + + // Check if this is a valid ruin location + const idx = map.locationToIndex(check.x, check.y) + const ruin = map.ruins.findIndex((l) => squareIntersects(l, check, 4)) + const wall = map.walls.findIndex((v, i) => !!v && squareIntersects(map.indexToLocation(i), check, 2)) + const paint = map.initialPaint[idx] + + let tower = undefined + for (const b of bodies.bodies.values()) { + if (squareIntersects(check, b.pos, 4)) { + tower = b + break + } + } + + if (tower || ruin !== -1 || wall !== -1 || paint) { + return false + } + + return true +} + export class WallsBrush extends SymmetricMapEditorBrush { + private readonly bodies: Bodies public readonly name = 'Walls' public readonly fields = { shouldAdd: { @@ -54,8 +83,9 @@ export class WallsBrush extends SymmetricMapEditorBrush { } } - constructor(map: StaticMap) { - super(map) + constructor(round: Round) { + super(round.map.staticMap) + this.bodies = round.bodies } public symmetricApply(x: number, y: number, fields: Record) { @@ -64,7 +94,17 @@ export class WallsBrush extends SymmetricMapEditorBrush { const pos = this.map.indexToLocation(idx) const ruin = this.map.ruins.findIndex((l) => squareIntersects(l, pos, 2)) const paint = this.map.initialPaint[idx] - if (ruin !== -1 || paint) return true + + let tower = undefined + for (const b of this.bodies.bodies.values()) { + if (squareIntersects(pos, b.pos, 2)) { + tower = b + break + } + } + + if (tower || ruin !== -1 || paint) return true + this.map.walls[idx] = 1 } @@ -94,6 +134,7 @@ export class WallsBrush extends SymmetricMapEditorBrush { } export class RuinsBrush extends SymmetricMapEditorBrush { + private readonly bodies: Bodies public readonly name = 'Ruins' public readonly fields = { shouldAdd: { @@ -102,26 +143,14 @@ export class RuinsBrush extends SymmetricMapEditorBrush { } } - constructor(map: StaticMap) { - super(map) + constructor(round: Round) { + super(round.map.staticMap) + this.bodies = round.bodies } public symmetricApply(x: number, y: number, fields: Record) { const add = (x: number, y: number) => { - // Check if ruin is too close to the border - if (x <= 1 || x >= this.map.width - 2 || y <= 1 || y >= this.map.height - 2) { - return true - } - - // Check if this is a valid ruin location - const pos = { x, y } - const idx = this.map.locationToIndex(x, y) - const ruin = this.map.ruins.findIndex((l) => squareIntersects(l, pos, 4)) - const wall = this.map.walls.findIndex( - (v, i) => !!v && squareIntersects(this.map.indexToLocation(i), pos, 2) - ) - const paint = this.map.initialPaint[idx] - if (ruin !== -1 || wall !== -1 || paint) { + if (!checkValidRuinPlacement({ x, y }, this.map, this.bodies)) { return true } @@ -145,6 +174,7 @@ export class RuinsBrush extends SymmetricMapEditorBrush { } export class PaintBrush extends SymmetricMapEditorBrush { + private readonly bodies: Bodies public readonly name = 'Paint' public readonly fields = { shouldAdd: { @@ -171,8 +201,9 @@ export class PaintBrush extends SymmetricMapEditorBrush { } } - constructor(map: CurrentMap) { - super(map) + constructor(round: Round) { + super(round.map) + this.bodies = round.bodies } public symmetricApply(x: number, y: number, fields: Record, robotOne: boolean) { @@ -217,6 +248,7 @@ export class PaintBrush extends SymmetricMapEditorBrush { } export class TowerBrush extends SymmetricMapEditorBrush { + private readonly bodies: Bodies public readonly name = 'Towers' public readonly fields = { isTower: { @@ -238,11 +270,9 @@ export class TowerBrush extends SymmetricMapEditorBrush { } } - constructor( - private readonly bodies: Bodies, - map: StaticMap - ) { - super(map) + constructor(round: Round) { + super(round.map.staticMap) + this.bodies = round.bodies } public symmetricApply(x: number, y: number, fields: Record, robotOne: boolean) { @@ -250,14 +280,10 @@ export class TowerBrush extends SymmetricMapEditorBrush { const isTower: boolean = fields.isTower.value const add = (x: number, y: number, team: Team) => { - // Check if this is a valid tower location const pos = { x, y } - const idx = this.map.locationToIndex(x, y) - const body = this.bodies.getBodyAtLocation(x, y) - const wall = this.map.walls[idx] - const ruin = this.map.ruins.findIndex((l) => squareIntersects(l, pos, 2)) - - if (body || wall || ruin !== -1) return null + if (!checkValidRuinPlacement(pos, this.map, this.bodies)) { + return null + } const id = this.bodies.getNextID() this.bodies.spawnBodyFromValues(id, towerType, team, pos) diff --git a/client/src/playback/Map.ts b/client/src/playback/Map.ts index 77f365b3..c1e4121a 100644 --- a/client/src/playback/Map.ts +++ b/client/src/playback/Map.ts @@ -9,6 +9,7 @@ import { DIVIDER_COLOR, TILE_COLOR, WALLS_COLOR, PAINT_COLORS, TEAM_COLORS, TEAM import * as renderUtils from '../util/RenderUtil' import { getImageIfLoaded } from '../util/ImageLoader' import { ClientConfig } from '../client-config' +import Round from './Round' export type Dimension = { minCorner: Vector @@ -121,14 +122,18 @@ export class CurrentMap { } if (config.showPaintMarkers) { + // Scale text by 0.5 because sometimes 0.5px text does not work + const markerA = this.markers[0][schemaIdx] if (markerA) { ctx.fillStyle = TEAM_COLORS[0] const label = markerA === 1 ? '1' : '2' // Primary/secondary - ctx.font = '0.5px monospace' + ctx.font = '1px monospace' ctx.shadowColor = 'black' ctx.shadowBlur = 4 - ctx.fillText(label, coords.x + 0.05, coords.y + 0.95) + ctx.scale(0.5, 0.5) + ctx.fillText(label, (coords.x + 0.05) * 2, (coords.y + 0.95) * 2) + ctx.scale(2, 2) ctx.shadowColor = '' ctx.shadowBlur = 0 } @@ -137,10 +142,12 @@ export class CurrentMap { if (markerB) { ctx.fillStyle = TEAM_COLORS[1] const label = markerB === 3 ? '1' : '2' // Primary/secondary - ctx.font = '0.5px monospace' + ctx.font = '1px monospace' ctx.shadowColor = 'black' ctx.shadowBlur = 4 - ctx.fillText(label, coords.x + 0.65, coords.y + 0.95) + ctx.scale(0.5, 0.5) + ctx.fillText(label, (coords.x + 0.65) * 2, (coords.y + 0.95) * 2) + ctx.scale(2, 2) ctx.shadowColor = '' ctx.shadowBlur = 0 } @@ -185,14 +192,8 @@ export class CurrentMap { return info } - getEditorBrushes() { - const brushes: MapEditorBrush[] = [ - // ruins brush - // tower brush - new PaintBrush(this), - new RuinsBrush(this.staticMap), - new WallsBrush(this.staticMap) - ] + getEditorBrushes(round: Round) { + const brushes: MapEditorBrush[] = [new PaintBrush(round), new RuinsBrush(round), new WallsBrush(round)] return brushes.concat(this.staticMap.getEditorBrushes()) } diff --git a/engine/src/main/battlecode/common/RobotController.java b/engine/src/main/battlecode/common/RobotController.java index 6735a729..6dee1a7f 100644 --- a/engine/src/main/battlecode/common/RobotController.java +++ b/engine/src/main/battlecode/common/RobotController.java @@ -132,6 +132,15 @@ public interface RobotController { */ UnitType getType(); + /** + * Returns how many allied towers are currently alive. + * + * @return the number of alive allied towers. + * + * @battlecode.doc.costlymethod + */ + int getNumberTowers(); + // *********************************** // ****** GENERAL VISION METHODS ***** // *********************************** @@ -527,7 +536,7 @@ public interface RobotController { boolean canMark(MapLocation loc); /** - * Checks if the location can be marked. + * Adds a mark at the given location. * * @param loc the location to mark * @param secondary whether the secondary color should be used @@ -735,8 +744,20 @@ public interface RobotController { * and only if one unit is a robot and the other is a tower. * * @param loc the location to send the message to - * @param messageContent an int representing the content of the - * message (up to 4 bytes) + * + * @battlecode.doc.costlymethod + */ + boolean canSendMessage(MapLocation loc); + + /** + * Returns true if the unit can send a message to a specific + * location, false otherwise. We can send a message to a location + * if it is within a specific distance and connected by paint, + * and only if one unit is a robot and the other is a tower. + * + * @param loc the location to send the message to + * @param messageContent the contents of the message. + * Does not affect whether or not the message can be sent. * * @battlecode.doc.costlymethod */ @@ -774,10 +795,10 @@ public interface RobotController { /** * Tests whether you can transfer paint to a given robot/tower. * - * You can give paint to an allied robot if you are a mopper and can act at the + * You can give paint to an allied robot/tower if you are a mopper and can act at the * given location. - * You can give/take paint from allied towers regardless of type, if you can act - * at the location. + * You can take paint from allied towers regardless of type, if you can act + * at the location. Pass in a negative number to take paint. * * @param loc the location of the robot/tower to transfer paint to * @param amount the amount of paint to transfer. Positive to give paint, @@ -789,7 +810,8 @@ public interface RobotController { /** * Transfers paint from the robot's stash to the stash of the allied - * robot or tower at loc. + * robot or tower at loc. Pass in a negative number to take paint, positive + * to give paint. * * @param loc the location of the robot/tower to transfer paint to * @param amount the amount of paint to transfer. Positive to give paint, @@ -799,6 +821,13 @@ public interface RobotController { */ void transferPaint(MapLocation loc, int amount) throws GameActionException; + /** + * Destroys the robot. + * + * @battlecode.doc.costlymethod + **/ + void disintegrate(); + /** * Causes your team to lose the game. It's like typing "gg." * @@ -831,7 +860,7 @@ public interface RobotController { * * @battlecode.doc.costlymethod */ - void setIndicatorDot(MapLocation loc, int red, int green, int blue); + void setIndicatorDot(MapLocation loc, int red, int green, int blue) throws GameActionException; /** * Draw a line on the game map for debugging purposes. @@ -844,7 +873,7 @@ public interface RobotController { * * @battlecode.doc.costlymethod */ - void setIndicatorLine(MapLocation startLoc, MapLocation endLoc, int red, int green, int blue); + void setIndicatorLine(MapLocation startLoc, MapLocation endLoc, int red, int green, int blue) throws GameActionException; /** * Adds a marker to the timeline at the current diff --git a/engine/src/main/battlecode/instrumenter/bytecode/resources/MethodCosts.txt b/engine/src/main/battlecode/instrumenter/bytecode/resources/MethodCosts.txt index e734bb77..6521aa0d 100644 --- a/engine/src/main/battlecode/instrumenter/bytecode/resources/MethodCosts.txt +++ b/engine/src/main/battlecode/instrumenter/bytecode/resources/MethodCosts.txt @@ -51,12 +51,14 @@ battlecode/common/RobotController/getMapWidth 1 tru battlecode/common/RobotController/getMovementCooldownTurns 1 true battlecode/common/RobotController/getRoundNum 1 true battlecode/common/RobotController/getTeam 1 true +battlecode/common/RobotController/getNumberTowers 5 true battlecode/common/RobotController/isActionReady 1 true battlecode/common/RobotController/isLocationOccupied 5 true battlecode/common/RobotController/isMovementReady 1 true battlecode/common/RobotController/move 0 true battlecode/common/RobotController/onTheMap 5 true battlecode/common/RobotController/readSharedArray 2 true +battlecode/common/RobotController/disintegrate 0 true battlecode/common/RobotController/resign 0 true battlecode/common/RobotController/senseNearbyRobots 100 true battlecode/common/RobotController/senseRobot 25 true diff --git a/engine/src/main/battlecode/world/GameWorld.java b/engine/src/main/battlecode/world/GameWorld.java index 801f0f22..7b35bbcb 100644 --- a/engine/src/main/battlecode/world/GameWorld.java +++ b/engine/src/main/battlecode/world/GameWorld.java @@ -133,6 +133,8 @@ public GameWorld(LiveMap gm, RobotControlProvider cp, GameMaker.MatchMaker match spawnRobot(robot.ID, robot.type, newLocation, robot.team); this.towerLocations.add(newLocation); towersByLoc[locationToIndex(newLocation)] = robot.team; + this.allRuinsByLoc[locationToIndex(newLocation)] = true; + this.allRuins.add(newLocation); } } @@ -787,7 +789,7 @@ private MapLocation[] getAllLocations() { public void processBeginningOfRound() { currentRound++; updateResourcePatterns(); - + this.getMatchMaker().startRound(currentRound); // Process beginning of each robot's round objectInfo.eachRobot((robot) -> { diff --git a/engine/src/main/battlecode/world/RobotControllerImpl.java b/engine/src/main/battlecode/world/RobotControllerImpl.java index 09284a9d..0c574739 100644 --- a/engine/src/main/battlecode/world/RobotControllerImpl.java +++ b/engine/src/main/battlecode/world/RobotControllerImpl.java @@ -5,6 +5,7 @@ import static battlecode.common.GameActionExceptionType.*; import battlecode.schema.Action; import battlecode.util.FlatHelpers; +import battlecode.instrumenter.RobotDeathException; import java.util.*; import java.util.stream.Collectors; @@ -155,6 +156,11 @@ public UnitType getType(){ return this.robot.getType(); } + @Override + public int getNumberTowers(){ + return this.gameWorld.getTeamInfo().getTotalNumberOfTowers(getTeam()); + } + // *********************************** // ****** GENERAL VISION METHODS ***** // *********************************** @@ -806,13 +812,17 @@ public void completeResourcePattern(MapLocation loc) throws GameActionException // ***************************** private void assertCanAttackSoldier(MapLocation loc) throws GameActionException { + assertIsActionReady(); assertCanActLocation(loc, UnitType.SOLDIER.actionRadiusSquared); if (this.robot.getPaint() < UnitType.SOLDIER.attackCost){ throw new GameActionException(CANT_DO_THAT, "Unit does not have enough paint to do a soldier attack"); } + if (this.gameWorld.getWall(loc)) + throw new GameActionException(CANT_DO_THAT, "Soldiers cannot attack walls!"); } private void assertCanAttackSplasher(MapLocation loc) throws GameActionException { + assertIsActionReady(); assertCanActLocation(loc, UnitType.SPLASHER.actionRadiusSquared); if (this.robot.getPaint() < UnitType.SPLASHER.attackCost){ throw new GameActionException(CANT_DO_THAT, "Unit does not have enough paint to do a splasher attack"); @@ -820,10 +830,13 @@ private void assertCanAttackSplasher(MapLocation loc) throws GameActionException } private void assertCanAttackMopper(MapLocation loc) throws GameActionException { + assertIsActionReady(); assertCanActLocation(loc, UnitType.MOPPER.actionRadiusSquared); if (this.robot.getPaint() < UnitType.MOPPER.attackCost){ throw new GameActionException(CANT_DO_THAT, "Unit does not have enough paint to do a mopper attack"); } + if (!this.gameWorld.isPassable(loc)) + throw new GameActionException(CANT_DO_THAT, "Moppers cannot attack squares with walls or ruins on them!"); } private void assertCanAttackTower(MapLocation loc) throws GameActionException { @@ -843,7 +856,6 @@ private void assertCanAttack(MapLocation loc) throws GameActionException { if (loc == null && !this.robot.getType().isTowerType()){ throw new GameActionException(CANT_DO_THAT, "Robot units must specify a location to attack"); } - assertIsActionReady(); // note: paint type is irrelevant for checking attack validity switch(this.robot.getType()) { @@ -875,7 +887,8 @@ public boolean canAttack(MapLocation loc) { @Override public void attack(MapLocation loc, boolean useSecondaryColor) throws GameActionException { assertCanAttack(loc); - this.robot.addActionCooldownTurns(this.robot.getType().actionCooldown); + if (this.robot.getType().isRobotType()) + this.robot.addActionCooldownTurns(this.robot.getType().actionCooldown); this.robot.attack(loc, useSecondaryColor); } @@ -953,6 +966,12 @@ private void assertCanSendMessage(MapLocation loc, Message message) throws GameA } } + @Override + public boolean canSendMessage(MapLocation loc) { + // use dummy message content as does not affect if message can be sent + return canSendMessage(loc, 0); + } + @Override public boolean canSendMessage(MapLocation loc, int messageContent) { try { @@ -960,7 +979,6 @@ public boolean canSendMessage(MapLocation loc, int messageContent) { assertCanSendMessage(loc, message); return true; } catch (GameActionException e) { - //System.out.println(e); return false; } } @@ -1039,6 +1057,11 @@ public void transferPaint(MapLocation loc, int amount) throws GameActionExceptio this.gameWorld.getMatchMaker().addTransferAction(robot.getID(), amount); } + @Override + public void disintegrate() { + throw new RobotDeathException(); + } + @Override public void resign() { Team team = getTeam(); @@ -1064,15 +1087,21 @@ public void setIndicatorString(String string) { } @Override - public void setIndicatorDot(MapLocation loc, int red, int green, int blue) { + public void setIndicatorDot(MapLocation loc, int red, int green, int blue) throws GameActionException{ assertNotNull(loc); + if (!this.gameWorld.getGameMap().onTheMap(loc)) + throw new GameActionException(CANT_DO_THAT, "Indicator dots should have map locations on the map!"); this.gameWorld.getMatchMaker().addIndicatorDot(getID(), loc, red, green, blue); } @Override - public void setIndicatorLine(MapLocation startLoc, MapLocation endLoc, int red, int green, int blue) { + public void setIndicatorLine(MapLocation startLoc, MapLocation endLoc, int red, int green, int blue) throws GameActionException{ assertNotNull(startLoc); assertNotNull(endLoc); + if (!this.gameWorld.getGameMap().onTheMap(startLoc)) + throw new GameActionException(CANT_DO_THAT, "Indicator lines should have map locations on the map!"); + if (!this.gameWorld.getGameMap().onTheMap(endLoc)) + throw new GameActionException(CANT_DO_THAT, "Indicator lines should have map locations on the map!"); this.gameWorld.getMatchMaker().addIndicatorLine(getID(), startLoc, endLoc, red, green, blue); } diff --git a/engine/src/main/battlecode/world/resources/DefaultLarge.map25 b/engine/src/main/battlecode/world/resources/DefaultLarge.map25 index 887d4857..0f16f52a 100644 Binary files a/engine/src/main/battlecode/world/resources/DefaultLarge.map25 and b/engine/src/main/battlecode/world/resources/DefaultLarge.map25 differ diff --git a/engine/src/main/battlecode/world/resources/DefaultMedium.map25 b/engine/src/main/battlecode/world/resources/DefaultMedium.map25 index efaba2d0..8d0c1ea3 100644 Binary files a/engine/src/main/battlecode/world/resources/DefaultMedium.map25 and b/engine/src/main/battlecode/world/resources/DefaultMedium.map25 differ diff --git a/engine/src/main/battlecode/world/resources/DefaultSmall.map25 b/engine/src/main/battlecode/world/resources/DefaultSmall.map25 index 3b7ba3b8..8ae4cb29 100644 Binary files a/engine/src/main/battlecode/world/resources/DefaultSmall.map25 and b/engine/src/main/battlecode/world/resources/DefaultSmall.map25 differ diff --git a/specs/specs.pdf b/specs/specs.pdf index fc83c38f..eb861200 100644 Binary files a/specs/specs.pdf and b/specs/specs.pdf differ