diff --git a/game-engine-interface/pom.xml b/game-engine-interface/pom.xml index f442bc2..b374129 100644 --- a/game-engine-interface/pom.xml +++ b/game-engine-interface/pom.xml @@ -6,7 +6,7 @@ za.co.entelect.challenge game-engine-interface - 1.0.0 + 1.0.1 diff --git a/game-engine-interface/src/main/java/za/co/entelect/challenge/game/contracts/game/GameEngine.java b/game-engine-interface/src/main/java/za/co/entelect/challenge/game/contracts/game/GameEngine.java index 43d2bee..d9b64e1 100644 --- a/game-engine-interface/src/main/java/za/co/entelect/challenge/game/contracts/game/GameEngine.java +++ b/game-engine-interface/src/main/java/za/co/entelect/challenge/game/contracts/game/GameEngine.java @@ -5,4 +5,5 @@ public interface GameEngine { boolean isGameComplete(GameMap gameMap); + } diff --git a/game-engine/core/pom.xml b/game-engine/core/pom.xml index c9ae450..4ae4252 100644 --- a/game-engine/core/pom.xml +++ b/game-engine/core/pom.xml @@ -5,7 +5,7 @@ game-engine za.co.entelect.challenge - 1.0.1 + 1.1.1 4.0.0 @@ -27,7 +27,7 @@ za.co.entelect.challenge domain - 1.0.1 + 1.1.1 compile diff --git a/game-engine/core/src/main/java/za/co/entelect/challenge/core/engine/TowerDefenseGameEngine.java b/game-engine/core/src/main/java/za/co/entelect/challenge/core/engine/TowerDefenseGameEngine.java index f33cd24..8bb7418 100644 --- a/game-engine/core/src/main/java/za/co/entelect/challenge/core/engine/TowerDefenseGameEngine.java +++ b/game-engine/core/src/main/java/za/co/entelect/challenge/core/engine/TowerDefenseGameEngine.java @@ -7,6 +7,10 @@ public class TowerDefenseGameEngine implements GameEngine { + public TowerDefenseGameEngine(String configLocation) { + GameConfig.initConfig(configLocation); + } + @Override public boolean isGameComplete(GameMap gameMap) { TowerDefenseGameMap towerDefenseGameMap = (TowerDefenseGameMap) gameMap; @@ -16,4 +20,5 @@ public boolean isGameComplete(GameMap gameMap) { return (towerDefenseGameMap.getDeadPlayers().size() > 0); } + } diff --git a/game-engine/core/src/main/java/za/co/entelect/challenge/core/entities/GameDetails.java b/game-engine/core/src/main/java/za/co/entelect/challenge/core/entities/GameDetails.java index 5716e20..ac5b01a 100644 --- a/game-engine/core/src/main/java/za/co/entelect/challenge/core/entities/GameDetails.java +++ b/game-engine/core/src/main/java/za/co/entelect/challenge/core/entities/GameDetails.java @@ -1,24 +1,33 @@ package za.co.entelect.challenge.core.entities; import za.co.entelect.challenge.config.GameConfig; +import za.co.entelect.challenge.entities.BuildingStats; import za.co.entelect.challenge.enums.BuildingType; +import za.co.entelect.challenge.factories.BuildingFactory; +import java.util.Arrays; import java.util.HashMap; public class GameDetails { + private int round; private int mapWidth; private int mapHeight; - private HashMap buildingPrices; + private int roundIncomeEnergy; + + @Deprecated + private HashMap buildingPrices = new HashMap<>(); - public GameDetails(int round){ + private HashMap buildingsStats = new HashMap<>(); + + public GameDetails(int round) { this.round = round; this.mapWidth = GameConfig.getMapWidth(); this.mapHeight = GameConfig.getMapHeight(); + this.roundIncomeEnergy = GameConfig.getRoundIncomeEnergy(); + + Arrays.asList(BuildingType.values()).forEach(bt -> buildingPrices.put(bt, BuildingFactory.createBuildingStats(bt).price)); - buildingPrices = new HashMap<>(); - buildingPrices.put(BuildingType.DEFENSE, GameConfig.getDefensePrice()); - buildingPrices.put(BuildingType.ATTACK, GameConfig.getAttackPrice()); - buildingPrices.put(BuildingType.ENERGY, GameConfig.getEnergyPrice()); + Arrays.stream(BuildingType.values()).forEach(bt -> buildingsStats.put(bt, BuildingFactory.createBuildingStats(bt))); } } diff --git a/game-engine/core/src/main/java/za/co/entelect/challenge/core/renderers/TowerDefenseTextMapRenderer.java b/game-engine/core/src/main/java/za/co/entelect/challenge/core/renderers/TowerDefenseTextMapRenderer.java index 775daff..83989a1 100644 --- a/game-engine/core/src/main/java/za/co/entelect/challenge/core/renderers/TowerDefenseTextMapRenderer.java +++ b/game-engine/core/src/main/java/za/co/entelect/challenge/core/renderers/TowerDefenseTextMapRenderer.java @@ -2,11 +2,10 @@ import za.co.entelect.challenge.config.GameConfig; import za.co.entelect.challenge.core.entities.CellStateContainer; -import za.co.entelect.challenge.entities.Building; -import za.co.entelect.challenge.entities.Missile; -import za.co.entelect.challenge.entities.TowerDefenseGameMap; -import za.co.entelect.challenge.entities.TowerDefensePlayer; +import za.co.entelect.challenge.entities.*; +import za.co.entelect.challenge.enums.BuildingType; import za.co.entelect.challenge.enums.PlayerType; +import za.co.entelect.challenge.factories.BuildingFactory; import za.co.entelect.challenge.game.contracts.game.GamePlayer; import za.co.entelect.challenge.game.contracts.map.GameMap; import za.co.entelect.challenge.game.contracts.renderer.GameMapRenderer; @@ -28,10 +27,11 @@ public String render(GameMap gameMap, GamePlayer gamePlayer) { stringBuilder.append("XXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n"); stringBuilder.append("\n"); - stringBuilder.append("****** BUILDING PRICES ******\n"); - stringBuilder.append("ATTACK : " + GameConfig.getAttackPrice() + "\n"); - stringBuilder.append("DEFEND : " + GameConfig.getDefensePrice() + "\n"); - stringBuilder.append("ENERGY : " + GameConfig.getEnergyPrice() + "\n"); + stringBuilder.append("****** BUILDING STATS ******\n"); + stringBuilder.append("type;" + BuildingStats.getTextHeader() + "\n"); + stringBuilder.append("ATTACK;" + BuildingFactory.createBuildingStats(BuildingType.ATTACK) + "\n"); + stringBuilder.append("DEFENSE;" + BuildingFactory.createBuildingStats(BuildingType.DEFENSE) + "\n"); + stringBuilder.append("ENERGY;" + BuildingFactory.createBuildingStats(BuildingType.ENERGY) + "\n"); stringBuilder.append("*****************************\n"); stringBuilder.append("\n"); @@ -159,28 +159,9 @@ private String getRowStringForPlayer(CellStateContainer[] row, int y){ return stringBuilderRow.toString(); } - private String padString(String stringToPad, int targetLength, PaddingDirection paddingDirection){ - String newString = stringToPad; - int difference = targetLength - stringToPad.length(); - - for (int i =0; i< difference; i++){ - if (paddingDirection == PaddingDirection.LEFT){ - newString = " " + newString; - }else{ - newString = newString + " "; - } - } - - return newString; - } - @Override public String commandPrompt(GamePlayer gamePlayer) { return ""; } - private enum PaddingDirection{ - LEFT, - RIGHT - } } diff --git a/game-engine/core/src/main/java/za/co/entelect/challenge/core/state/RoundState.java b/game-engine/core/src/main/java/za/co/entelect/challenge/core/state/RoundState.java deleted file mode 100644 index 112754d..0000000 --- a/game-engine/core/src/main/java/za/co/entelect/challenge/core/state/RoundState.java +++ /dev/null @@ -1,29 +0,0 @@ -package za.co.entelect.challenge.core.state; - -import za.co.entelect.challenge.entities.TowerDefensePlayer; - -public class RoundState { - - private String GameVersion; - private String GameLevel; - private String Round; - private String MapDimension; - private String Phase; - private TowerDefensePlayer towerDefensePlayerThis; - private TowerDefensePlayer towerDefensePlayerOther; - private String map; - - - @Override - public String toString() { - return "RoundState{" + - "GameVersion='" + GameVersion + '\'' + - ", GameLevel='" + GameLevel + '\'' + - ", Round='" + Round + '\'' + - ", MapDimension='" + MapDimension + '\'' + - ", Phase='" + Phase + '\'' + - ", PlayerA=" + towerDefensePlayerThis + - ", PlayerB=" + towerDefensePlayerOther + - '}'; - } -} diff --git a/game-engine/domain/pom.xml b/game-engine/domain/pom.xml index a4665e5..af9bb92 100644 --- a/game-engine/domain/pom.xml +++ b/game-engine/domain/pom.xml @@ -5,7 +5,7 @@ game-engine za.co.entelect.challenge - 1.0.1 + 1.1.1 4.0.0 diff --git a/game-engine/domain/src/main/java/za/co/entelect/challenge/config/GameConfig.java b/game-engine/domain/src/main/java/za/co/entelect/challenge/config/GameConfig.java index d1f7532..f368c28 100644 --- a/game-engine/domain/src/main/java/za/co/entelect/challenge/config/GameConfig.java +++ b/game-engine/domain/src/main/java/za/co/entelect/challenge/config/GameConfig.java @@ -8,14 +8,16 @@ public class GameConfig { private static Configuration configuration; - static { - Configurations configurations = new Configurations(); + public static void initConfig(String configLocation) { + if (configuration == null) { + Configurations configurations = new Configurations(); - try { - configuration = configurations.properties(GameConfig.class.getResource("/game-config.properties")); + try { + configuration = configurations.properties(configLocation); - } catch (ConfigurationException e) { - throw new RuntimeException("Unable to initialise configuration, please have a look at the inner exception.", e); + } catch (ConfigurationException e) { + throw new RuntimeException("Unable to initialise configuration, please have a look at the inner exception.", e); + } } } diff --git a/game-engine/domain/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java b/game-engine/domain/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java new file mode 100644 index 0000000..29c6861 --- /dev/null +++ b/game-engine/domain/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java @@ -0,0 +1,43 @@ +package za.co.entelect.challenge.entities; + +public class BuildingStats { + + public int health; + public int constructionTime; + public int price; + public int weaponDamage; + public int weaponSpeed; + public int weaponCooldownPeriod; + public int energyGeneratedPerTurn; + public int destroyMultiplier; + public int constructionScore; + + public BuildingStats(Building building) { + this.health = building.getHealth(); + this.constructionTime = building.getConstructionTimeLeft(); + this.price = building.getPrice(); + this.weaponDamage = building.getWeaponDamage(); + this.weaponSpeed = building.getWeaponSpeed(); + this.weaponCooldownPeriod = building.getWeaponCooldownPeriod(); + this.destroyMultiplier = building.getDestroyMultiplier(); + this.constructionScore = building.getConstructionScore(); + this.energyGeneratedPerTurn = building.getEnergyGeneratedPerTurn(); + } + + public static String getTextHeader() { + return "health;constructionTime;price;weaponDamage;weaponSpeed;weaponCooldownPeriod;energyGeneratedPerTurn;destroyMultiplier;constructionScore"; + } + + @Override + public String toString() { + return health + ";" + + constructionTime + ";" + + price + ";" + + weaponDamage + ";" + + weaponSpeed + ";" + + weaponCooldownPeriod + ";" + + energyGeneratedPerTurn + ";" + + destroyMultiplier + ";" + + constructionScore + ";"; + } +} diff --git a/game-engine/domain/src/main/java/za/co/entelect/challenge/factories/BuildingFactory.java b/game-engine/domain/src/main/java/za/co/entelect/challenge/factories/BuildingFactory.java index c4f1e84..d2b5a87 100644 --- a/game-engine/domain/src/main/java/za/co/entelect/challenge/factories/BuildingFactory.java +++ b/game-engine/domain/src/main/java/za/co/entelect/challenge/factories/BuildingFactory.java @@ -2,6 +2,7 @@ import za.co.entelect.challenge.config.GameConfig; import za.co.entelect.challenge.entities.Building; +import za.co.entelect.challenge.entities.BuildingStats; import za.co.entelect.challenge.enums.BuildingType; import za.co.entelect.challenge.enums.PlayerType; @@ -55,4 +56,9 @@ public static Building createBuilding(int x, int y, BuildingType buildingType, P return building; } + public static BuildingStats createBuildingStats(BuildingType buildingType) { + Building building = createBuilding(0, 0, buildingType, PlayerType.A); + return new BuildingStats(building); + } + } diff --git a/game-engine/pom.xml b/game-engine/pom.xml index c20ceba..ce6784a 100644 --- a/game-engine/pom.xml +++ b/game-engine/pom.xml @@ -5,7 +5,7 @@ za.co.entelect.challenge game-engine - 1.0.1 + 1.1.1 domain core @@ -16,7 +16,7 @@ 1.8 - 1.0.0 + 1.0.1 4.12 2.2 2.8.2 diff --git a/game-rules.md b/game-rules.md index dda23de..91ce8e2 100644 --- a/game-rules.md +++ b/game-rules.md @@ -10,7 +10,7 @@ * As a player, you will always be player A. * The player can only build buildings in their half of the map. * The coordinates for a cell on the map takes the form of **'X,Y'** starting from 0, e.g. the coordinates **'0,0'** will be the top left cell. -* The entire map, player information, and building information will be visible to both players, including the opposing player's units. +* The entire map, player information, and building information will be visible to both players, including the opposing player's buildings. **{X} and {Y} will be variable.** diff --git a/game-runner/config.json b/game-runner/config.json index 814e847..3bfe8ec 100644 --- a/game-runner/config.json +++ b/game-runner/config.json @@ -1,6 +1,8 @@ { "round-state-output-location": "./tower-defence-matches", + "game-config-file-location": "./game-config.properties", + "verbose-mode": true, "max-runtime-ms": 2000, - "player-a": "../starter-bots/kotlin", - "player-b": "../starter-bots/python3" + "player-a": "../starter-bots/java", + "player-b": "../reference-bot/java" } \ No newline at end of file diff --git a/game-runner/game-config.properties b/game-runner/game-config.properties new file mode 100644 index 0000000..bdce9de --- /dev/null +++ b/game-runner/game-config.properties @@ -0,0 +1,45 @@ +#Game Config +game.config.map-width = 8 +game.config.map-height = 4 +game.config.max-rounds = 400 +game.config.start-energy = 20 +game.config.round-income-energy = 5 +game.config.starting-health = 100 +game.config.health-score-multiplier = 100 +game.config.energy-score-multiplier = 1 + +#Basic Wall Config +game.config.defense.config.health = 20 +game.config.defense.config.construction-time-left = 3 +game.config.defense.config.price = 30 +game.config.defense.config.weapon-damage = 0 +game.config.defense.config.weapon-speed = 0 +game.config.defense.config.weapon-cooldown-period = 0 +game.config.defense.config.icon = D +game.config.defense.config.destroy-multiplier = 1 +game.config.defense.config.construction-score = 1 +game.config.defense.config.energy-Produced-per-turn = 0 + +#Basic Turret Config +game.config.attack.config.health = 5 +game.config.attack.config.construction-time-left = 1 +game.config.attack.config.price = 30 +game.config.attack.config.weapon-damage = 5 +game.config.attack.config.weapon-speed = 1 +game.config.attack.config.weapon-cooldown-period = 3 +game.config.attack.config.icon = A +game.config.attack.config.destroy-multiplier = 1 +game.config.attack.config.construction-score = 1 +game.config.attack.config.energy-Produced-per-turn = 0 + +#Basic Energy Generator Config +game.config.energy.config.health = 5 +game.config.energy.config.construction-time-left = 1 +game.config.energy.config.price = 20 +game.config.energy.config.weapon-damage = 0 +game.config.energy.config.weapon-speed = 0 +game.config.energy.config.weapon-cooldown-period = 0 +game.config.energy.config.icon = E +game.config.energy.config.destroy-multiplier = 1 +game.config.energy.config.construction-score = 1 +game.config.energy.config.energy-Produced-per-turn = 3 diff --git a/game-runner/pom.xml b/game-runner/pom.xml index 3a5cf76..8961470 100644 --- a/game-runner/pom.xml +++ b/game-runner/pom.xml @@ -6,14 +6,14 @@ za.co.entelect.challenge game-runner - 1.0.0 + 1.1.1 1.8 0.9.11 2.8.2 - 1.0.0 - 1.0.1 + 1.0.1 + 1.1.1 @@ -42,6 +42,16 @@ commons-exec 1.3 + + org.apache.logging.log4j + log4j-api + 2.11.0 + + + org.apache.logging.log4j + log4j-core + 2.11.0 + @@ -77,6 +87,11 @@ + + + src/main/resources + + diff --git a/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/Config.java b/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/Config.java index 0c4dc01..83273e5 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/Config.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/Config.java @@ -14,4 +14,10 @@ public class Config { @SerializedName("round-state-output-location") public String roundStateOutputLocation; + @SerializedName("game-config-file-location") + public String gameConfigFileLocation; + + @SerializedName("verbose-mode") + public boolean isVerbose; + } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/GameBootstrapper.java b/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/GameBootstrapper.java index c57b403..ff31e83 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/GameBootstrapper.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/bootstrapper/GameBootstrapper.java @@ -2,15 +2,17 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; import za.co.entelect.challenge.botrunners.BotRunner; import za.co.entelect.challenge.botrunners.BotRunnerFactory; import za.co.entelect.challenge.core.engine.TowerDefenseGameEngine; import za.co.entelect.challenge.core.engine.TowerDefenseGameMapGenerator; import za.co.entelect.challenge.core.engine.TowerDefenseRoundProcessor; -import za.co.entelect.challenge.engine.exceptions.InvalidRunnerState; import za.co.entelect.challenge.engine.runner.GameEngineRunner; import za.co.entelect.challenge.entities.BotMetaData; -import za.co.entelect.challenge.enums.BotLanguage; import za.co.entelect.challenge.game.contracts.map.GameMap; import za.co.entelect.challenge.game.contracts.player.Player; import za.co.entelect.challenge.player.BotPlayer; @@ -26,17 +28,19 @@ import java.util.function.Consumer; public class GameBootstrapper { - + private static final Logger log = LogManager.getLogger(GameBootstrapper.class); + private GameEngineRunner gameEngineRunner; private static String gameName; public static void main(String[] args) { + GameBootstrapper gameBootstrapper = new GameBootstrapper(); try { Config config = gameBootstrapper.loadConfig(); - gameBootstrapper.prepareEngineRunner(); + gameBootstrapper.prepareEngineRunner(config); gameBootstrapper.prepareHandlers(); gameBootstrapper.prepareGame(config); @@ -60,10 +64,10 @@ private Config loadConfig() throws Exception { } } - private void prepareEngineRunner() { + private void prepareEngineRunner(Config config) { gameEngineRunner = new GameEngineRunner(); - gameEngineRunner.setGameEngine(new TowerDefenseGameEngine()); + gameEngineRunner.setGameEngine(new TowerDefenseGameEngine(config.gameConfigFileLocation)); gameEngineRunner.setGameMapGenerator(new TowerDefenseGameMapGenerator()); gameEngineRunner.setGameRoundProcessor(new TowerDefenseRoundProcessor()); } @@ -87,6 +91,12 @@ private void prepareGame(Config config) throws Exception { gameEngineRunner.preparePlayers(players); gameEngineRunner.prepareGameMap(); + + if (config.isVerbose) { + Configurator.setRootLevel(Level.DEBUG); + } else { + Configurator.setRootLevel(Level.ERROR); + } } private void parsePlayer(String playerConfig, List players, String playerNumber, int maximumBotRuntimeMilliSeconds) throws Exception { @@ -127,9 +137,9 @@ private Consumer getFirstPhaseHandler() { private BiConsumer getRoundCompleteHandler() { return (gameMap, round) -> { - System.out.println("======================================="); - System.out.println("Round ended " + round); - System.out.println("======================================="); + log.info("======================================="); + log.info("Round ended " + round); + log.info("======================================="); }; } @@ -147,13 +157,13 @@ private BiConsumer> getGameCompleteHandler() { } if (winner == null) { - System.out.println("======================================="); - System.out.println("The game ended in a tie"); - System.out.println("======================================="); + log.info("======================================="); + log.info("The game ended in a tie"); + log.info("======================================="); } else { - System.out.println("======================================="); - System.out.println("The winner is: " + winner.getName()); - System.out.println("======================================="); + log.info("======================================="); + log.info("The winner is: " + winner.getName()); + log.info("======================================="); } BufferedWriter bufferedWriter = null; @@ -174,17 +184,17 @@ private BiConsumer> getGameCompleteHandler() { private BiConsumer getRoundStartingHandler() { return (gameMap, round) -> { - System.out.println("======================================="); - System.out.println("Starting round " + round); - System.out.println("======================================="); + log.info("======================================="); + log.info("Starting round " + round); + log.info("======================================="); }; } private Consumer getGameStartedHandler() { return gameMap -> { - System.out.println("======================================="); - System.out.println("Starting game"); - System.out.println("======================================="); + log.info("======================================="); + log.info("Starting game"); + log.info("======================================="); }; } } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/BotRunnerFactory.java b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/BotRunnerFactory.java index 4010f45..7e60b86 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/BotRunnerFactory.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/BotRunnerFactory.java @@ -22,6 +22,12 @@ public static BotRunner createBotRunner(BotMetaData botMetaData, int timeoutInMi return new Python3BotRunner(botMetaData, timeoutInMilliseconds); case KOTLIN: return new KotlinBotRunner(botMetaData, timeoutInMilliseconds); + case GOLANG: + return new GolangBotRunner(botMetaData, timeoutInMilliseconds); + case HASKELL: + return new HaskellBotRunner(botMetaData, timeoutInMilliseconds); + case PHP: + return new PHPBotRunner(botMetaData, timeoutInMilliseconds); default: break; } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/GolangBotRunner.java b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/GolangBotRunner.java new file mode 100644 index 0000000..629a442 --- /dev/null +++ b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/GolangBotRunner.java @@ -0,0 +1,17 @@ +package za.co.entelect.challenge.botrunners; + +import za.co.entelect.challenge.entities.BotMetaData; +import java.io.IOException; + +public class GolangBotRunner extends BotRunner { + + public GolangBotRunner(BotMetaData botMetaData, int timoutInMilis) { + super(botMetaData, timoutInMilis); + } + + @Override + protected String runBot() throws IOException { + String line = "go run \"" + this.getBotDirectory() + "/" + this.getBotFileName() + "\""; + return RunSimpleCommandLineCommand(line, 0); + } +} diff --git a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/HaskellBotRunner.java b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/HaskellBotRunner.java new file mode 100644 index 0000000..9d2dc54 --- /dev/null +++ b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/HaskellBotRunner.java @@ -0,0 +1,25 @@ +package za.co.entelect.challenge.botrunners; + +import za.co.entelect.challenge.entities.BotMetaData; + +import java.io.IOException; + +public class HaskellBotRunner extends BotRunner { + + public HaskellBotRunner(BotMetaData botMetaData, int timeoutInMilliseconds) { + super(botMetaData, timeoutInMilliseconds); + } + + @Override + protected String runBot() throws IOException { + String line; + + if(System.getProperty("os.name").contains("Windows")) { + line = "cmd /c \"" + this.getBotFileName() + "\""; + } else { + line = "\"./" + this.getBotFileName() + "\""; + } + + return RunSimpleCommandLineCommand(line, 0); + } +} diff --git a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/PHPBotRunner.java b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/PHPBotRunner.java new file mode 100644 index 0000000..b6e71f5 --- /dev/null +++ b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/PHPBotRunner.java @@ -0,0 +1,18 @@ +package za.co.entelect.challenge.botrunners; + +import za.co.entelect.challenge.entities.BotMetaData; + +import java.io.IOException; + +public class PHPBotRunner extends BotRunner { + + public PHPBotRunner(BotMetaData botMetaData, int timeoutInMilliseconds) { + super(botMetaData, timeoutInMilliseconds); + } + + @Override + protected String runBot() throws IOException { + String line = "php \"" + this.getBotFileName() + "\""; + return RunSimpleCommandLineCommand(line, 0); + } +} diff --git a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python2BotRunner.java b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python2BotRunner.java index 88e9bf5..727b8f1 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python2BotRunner.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python2BotRunner.java @@ -12,7 +12,14 @@ public Python2BotRunner(BotMetaData botMetaData, int timeoutInMilliseconds) { @Override protected String runBot() throws IOException { - String line = "py -2 \"" + this.getBotFileName() + "\""; + String line; + + if(System.getProperty("os.name").contains("Windows")) { + line = "py -2 \"" + this.getBotFileName() + "\""; + } else { + line = "python2 \"" + this.getBotFileName() + "\""; + } + return RunSimpleCommandLineCommand(line, 0); } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python3BotRunner.java b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python3BotRunner.java index ce49143..3eb51bb 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python3BotRunner.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/botrunners/Python3BotRunner.java @@ -12,7 +12,14 @@ public Python3BotRunner(BotMetaData botMetaData, int timeoutInMilliseconds) { @Override protected String runBot() throws IOException { - String line = "py -3 \"" + this.getBotFileName() + "\""; + String line; + + if(System.getProperty("os.name").contains("Windows")) { + line = "py -3 \"" + this.getBotFileName() + "\""; + } else { + line = "python3 \"" + this.getBotFileName() + "\""; + } + return RunSimpleCommandLineCommand(line, 0); } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/GameEngineRunner.java b/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/GameEngineRunner.java index c3763f7..cb904e5 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/GameEngineRunner.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/GameEngineRunner.java @@ -1,5 +1,7 @@ package za.co.entelect.challenge.engine.runner; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import za.co.entelect.challenge.core.renderers.TowerDefenseConsoleMapRenderer; import za.co.entelect.challenge.engine.exceptions.InvalidRunnerState; import za.co.entelect.challenge.game.contracts.command.RawCommand; @@ -17,6 +19,8 @@ public class GameEngineRunner { + private static final Logger log = LogManager.getLogger(GameEngineRunner.class); + public Consumer firstPhaseHandler; public Consumer gameStartedHandler; public BiConsumer roundCompleteHandler; @@ -95,9 +99,12 @@ private void runInitialPhase() throws Exception { } } + private void processRound() throws Exception { + TowerDefenseConsoleMapRenderer renderer = new TowerDefenseConsoleMapRenderer(); - System.out.println(renderer.render(gameMap, players.get(0).getGamePlayer())); + //Only execute the render if the log mode is in INFO. + log.info(() -> renderer.render(gameMap, players.get(0).getGamePlayer())); gameMap.setCurrentRound(gameMap.getCurrentRound() + 1); diff --git a/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/RunnerRoundProcessor.java b/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/RunnerRoundProcessor.java index 37e24fe..8a36c23 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/RunnerRoundProcessor.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/engine/runner/RunnerRoundProcessor.java @@ -1,5 +1,7 @@ package za.co.entelect.challenge.engine.runner; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import za.co.entelect.challenge.engine.exceptions.InvalidCommandException; import za.co.entelect.challenge.engine.exceptions.InvalidOperationException; import za.co.entelect.challenge.game.contracts.command.RawCommand; @@ -13,6 +15,7 @@ import java.util.Hashtable; public class RunnerRoundProcessor { + private static final Logger log = LogManager.getLogger(RunnerRoundProcessor.class); private GameMap gameMap; private GameRoundProcessor gameRoundProcessor; @@ -35,7 +38,7 @@ boolean processRound() throws Exception { boolean processed = gameRoundProcessor.processRound(gameMap, commandsToProcess); ArrayList errorList = gameRoundProcessor.getErrorList(); //TODO: Remove later - System.out.println("Error List: " + Arrays.toString(errorList.toArray())); + log.info("Error List: " + Arrays.toString(errorList.toArray())); roundProcessed = true; return processed; @@ -48,7 +51,7 @@ void addPlayerCommand(Player player, RawCommand command) { commandsToProcess.put(player.getGamePlayer(), command); } catch (InvalidCommandException e) { - e.printStackTrace(); + log.error(e.getStackTrace()); } } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/enums/BotLanguage.java b/game-runner/src/main/java/za/co/entelect/challenge/enums/BotLanguage.java index ba72f63..63ec225 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/enums/BotLanguage.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/enums/BotLanguage.java @@ -30,5 +30,14 @@ public enum BotLanguage { @SerializedName("kotlin") KOTLIN, + + @SerializedName("php") + PHP, + + @SerializedName("haskell") + HASKELL, + + @SerializedName("golang") + GOLANG, } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/player/BotPlayer.java b/game-runner/src/main/java/za/co/entelect/challenge/player/BotPlayer.java index b91b8e1..ff8d04f 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/player/BotPlayer.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/player/BotPlayer.java @@ -1,9 +1,12 @@ package za.co.entelect.challenge.player; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import za.co.entelect.challenge.botrunners.BotRunner; import za.co.entelect.challenge.core.renderers.TowerDefenseConsoleMapRenderer; import za.co.entelect.challenge.core.renderers.TowerDefenseJsonGameMapRenderer; import za.co.entelect.challenge.core.renderers.TowerDefenseTextMapRenderer; +import za.co.entelect.challenge.engine.runner.GameEngineRunner; import za.co.entelect.challenge.game.contracts.command.RawCommand; import za.co.entelect.challenge.game.contracts.map.GameMap; import za.co.entelect.challenge.game.contracts.player.Player; @@ -24,6 +27,8 @@ public class BotPlayer extends Player { private BotRunner botRunner; private String saveStateLocation; + private static final Logger log = LogManager.getLogger(BotPlayer.class); + public BotPlayer(String name, BotRunner botRunner, String saveStateLocation) { super(name); @@ -66,7 +71,7 @@ public void newRoundStarted(GameMap gameMap) { } scanner.close(); } catch (FileNotFoundException e) { - System.out.println(String.format("File %s not found", botRunner.getBotDirectory() + "/" + BOT_COMMAND)); + log.info(String.format("File %s not found", botRunner.getBotDirectory() + "/" + BOT_COMMAND)); } try{ writeRoundStateData(playerSpecificJsonState, playerSpecificTextState, @@ -121,9 +126,9 @@ private String runBot(String state, String textState) throws IOException { try { botConsoleOutput = botRunner.run(); }catch (IOException e){ - System.out.println("Bot execution failed: " + e.getLocalizedMessage()); + log.info("Bot execution failed: " + e.getLocalizedMessage()); } - System.out.println("BotRunner Started."); + log.info("BotRunner Started."); return botConsoleOutput; } @@ -134,20 +139,20 @@ public void gameEnded(GameMap gameMap) { @Override public void playerKilled(GameMap gameMap) { - System.out.println(String.format("Player %s has been killed", getName())); + log.info(String.format("Player %s has been killed", getName())); } @Override public void playerCommandFailed(GameMap gameMap, String reason) { - System.out.println(String.format("Could not process player command: %s", reason)); + log.info(String.format("Could not process player command: %s", reason)); } @Override public void firstRoundFailed(GameMap gameMap, String reason) { - System.out.println(reason); - System.out.println("The first round has failed."); - System.out.println("The round will now restart and both players will have to try again"); - System.out.println("Press any key to continue"); + log.info(reason); + log.info("The first round has failed."); + log.info("The round will now restart and both players will have to try again"); + log.info("Press any key to continue"); scanner.nextLine(); } diff --git a/game-runner/src/main/java/za/co/entelect/challenge/player/ConsolePlayer.java b/game-runner/src/main/java/za/co/entelect/challenge/player/ConsolePlayer.java index 7a27ce6..9683a12 100644 --- a/game-runner/src/main/java/za/co/entelect/challenge/player/ConsolePlayer.java +++ b/game-runner/src/main/java/za/co/entelect/challenge/player/ConsolePlayer.java @@ -1,6 +1,9 @@ package za.co.entelect.challenge.player; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import za.co.entelect.challenge.core.renderers.TowerDefenseConsoleMapRenderer; +import za.co.entelect.challenge.engine.runner.GameEngineRunner; import za.co.entelect.challenge.game.contracts.command.RawCommand; import za.co.entelect.challenge.game.contracts.map.GameMap; import za.co.entelect.challenge.game.contracts.player.Player; @@ -10,6 +13,8 @@ public class ConsolePlayer extends Player { + private static final Logger log = LogManager.getLogger(ConsolePlayer.class); + private GameMapRenderer gameMapRenderer; private Scanner scanner; @@ -29,10 +34,10 @@ public void startGame(GameMap gameMap) { public void newRoundStarted(GameMap gameMap) { String output = gameMapRenderer.render(gameMap, getGamePlayer()); - System.out.println(output); + log.info(output); String inputPrompt = gameMapRenderer.commandPrompt(getGamePlayer()); - System.out.println(inputPrompt); + log.info(inputPrompt); String consoleInput = scanner.nextLine(); @@ -47,20 +52,20 @@ public void gameEnded(GameMap gameMap) { @Override public void playerKilled(GameMap gameMap) { - System.out.println(String.format("Player %s has been killed", getName())); + log.info(String.format("Player %s has been killed", getName())); } @Override public void playerCommandFailed(GameMap gameMap, String reason) { - System.out.println(String.format("Could not process player command: %s", reason)); + log.info(String.format("Could not process player command: %s", reason)); } @Override public void firstRoundFailed(GameMap gameMap, String reason) { - System.out.println(reason); - System.out.println("The first round has failed."); - System.out.println("The round will now restart and both players will have to try again"); - System.out.println("Press any key to continue"); + log.info(reason); + log.info("The first round has failed."); + log.info("The round will now restart and both players will have to try again"); + log.info("Press any key to continue"); scanner.nextLine(); } diff --git a/game-runner/src/main/resources/log4j2.properties b/game-runner/src/main/resources/log4j2.properties new file mode 100644 index 0000000..96feb2e --- /dev/null +++ b/game-runner/src/main/resources/log4j2.properties @@ -0,0 +1,12 @@ +name=PropertiesConfig +property.filename = logs +appenders = console + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %msg%n + +rootLogger.level = debug +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = STDOUT \ No newline at end of file diff --git a/reference-bot/java/pom.xml b/reference-bot/java/pom.xml index 9b07a0e..3cd38d8 100644 --- a/reference-bot/java/pom.xml +++ b/reference-bot/java/pom.xml @@ -6,7 +6,7 @@ za.co.entelect.challenge reference-bot - 1.0-SNAPSHOT + 1.1-SNAPSHOT diff --git a/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java b/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java index 16638fd..61624f6 100644 --- a/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java +++ b/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java @@ -13,31 +13,34 @@ import java.util.stream.Collectors; public class Bot { + private GameState gameState; /** * Constructor + * * @param gameState the game state **/ - public Bot(GameState gameState){ + public Bot(GameState gameState) { this.gameState = gameState; gameState.getGameMap(); } /** * Run + * * @return the result **/ - public String run(){ + public String run() { String command = ""; //If the enemy has an attack building and I don't have a blocking wall, then block from the front. - for (int i = 0; i < gameState.gameDetails.mapHeight; i++){ + for (int i = 0; i < gameState.gameDetails.mapHeight; i++) { int enemyAttackOnRow = getAllBuildingsForPlayer(PlayerType.B, b -> b.buildingType == BuildingType.ATTACK, i).size(); int myDefenseOnRow = getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.DEFENSE, i).size(); - if (enemyAttackOnRow > 0 && myDefenseOnRow == 0){ - if ( canAffordBuilding(BuildingType.DEFENSE)) + if (enemyAttackOnRow > 0 && myDefenseOnRow == 0) { + if (canAffordBuilding(BuildingType.DEFENSE)) command = placeBuildingInRowFromFront(BuildingType.DEFENSE, i); else command = ""; @@ -51,7 +54,7 @@ public String run(){ int enemyAttackOnRow = getAllBuildingsForPlayer(PlayerType.B, b -> b.buildingType == BuildingType.ATTACK, i).size(); int myEnergyOnRow = getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.ENERGY, i).size(); - if (enemyAttackOnRow == 0 && myEnergyOnRow == 0 ) { + if (enemyAttackOnRow == 0 && myEnergyOnRow == 0) { if (canAffordBuilding(BuildingType.ENERGY)) command = placeBuildingInRowFromBack(BuildingType.ENERGY, i); break; @@ -60,21 +63,21 @@ public String run(){ } //If I have a defense building on a row, then build an attack building behind it. - if (command.equals("")){ + if (command.equals("")) { for (int i = 0; i < gameState.gameDetails.mapHeight; i++) { - if ( getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.DEFENSE, i).size() > 0 - && canAffordBuilding(BuildingType.ATTACK)){ + if (getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.DEFENSE, i).size() > 0 + && canAffordBuilding(BuildingType.ATTACK)) { command = placeBuildingInRowFromFront(BuildingType.ATTACK, i); } } } //If I don't need to do anything then either attack or defend randomly based on chance (65% attack, 35% defense). - if (command.equals("")){ - if (getEnergy(PlayerType.A) >= getMostExpensiveBuildingPrice()){ - if ((new Random()).nextInt(100) <= 35){ + if (command.equals("")) { + if (getEnergy(PlayerType.A) >= getMostExpensiveBuildingPrice()) { + if ((new Random()).nextInt(100) <= 35) { return placeBuildingRandomlyFromFront(BuildingType.DEFENSE); - }else{ + } else { return placeBuildingRandomlyFromBack(BuildingType.ATTACK); } } @@ -85,13 +88,14 @@ && canAffordBuilding(BuildingType.ATTACK)){ /** * Place building in a random row nearest to the back + * * @param buildingType the building type * @return the result **/ - private String placeBuildingRandomlyFromBack(BuildingType buildingType){ - for (int i = 0; i < gameState.gameDetails.mapWidth/ 2; i ++){ + private String placeBuildingRandomlyFromBack(BuildingType buildingType) { + for (int i = 0; i < gameState.gameDetails.mapWidth / 2; i++) { List listOfFreeCells = getListOfEmptyCellsForColumn(i); - if (!listOfFreeCells.isEmpty()){ + if (!listOfFreeCells.isEmpty()) { CellStateContainer pickedCell = listOfFreeCells.get((new Random()).nextInt(listOfFreeCells.size())); return buildCommand(pickedCell.x, pickedCell.y, buildingType); } @@ -101,13 +105,14 @@ private String placeBuildingRandomlyFromBack(BuildingType buildingType){ /** * Place building in a random row nearest to the front + * * @param buildingType the building type * @return the result **/ - private String placeBuildingRandomlyFromFront(BuildingType buildingType){ - for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--){ + private String placeBuildingRandomlyFromFront(BuildingType buildingType) { + for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--) { List listOfFreeCells = getListOfEmptyCellsForColumn(i); - if (!listOfFreeCells.isEmpty()){ + if (!listOfFreeCells.isEmpty()) { CellStateContainer pickedCell = listOfFreeCells.get((new Random()).nextInt(listOfFreeCells.size())); return buildCommand(pickedCell.x, pickedCell.y, buildingType); } @@ -117,13 +122,14 @@ private String placeBuildingRandomlyFromFront(BuildingType buildingType){ /** * Place building in row y nearest to the front + * * @param buildingType the building type - * @param y the y + * @param y the y * @return the result **/ - private String placeBuildingInRowFromFront(BuildingType buildingType, int y){ - for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--){ - if (isCellEmpty(i, y)){ + private String placeBuildingInRowFromFront(BuildingType buildingType, int y) { + for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--) { + if (isCellEmpty(i, y)) { return buildCommand(i, y, buildingType); } } @@ -132,13 +138,14 @@ private String placeBuildingInRowFromFront(BuildingType buildingType, int y){ /** * Place building in row y nearest to the back + * * @param buildingType the building type - * @param y the y + * @param y the y * @return the result **/ - private String placeBuildingInRowFromBack(BuildingType buildingType, int y){ - for (int i = 0; i < gameState.gameDetails.mapWidth / 2; i++){ - if (isCellEmpty(i, y)){ + private String placeBuildingInRowFromBack(BuildingType buildingType, int y) { + for (int i = 0; i < gameState.gameDetails.mapWidth / 2; i++) { + if (isCellEmpty(i, y)) { return buildCommand(i, y, buildingType); } } @@ -147,23 +154,25 @@ private String placeBuildingInRowFromBack(BuildingType buildingType, int y){ /** * Construct build command - * @param x the x - * @param y the y + * + * @param x the x + * @param y the y * @param buildingType the building type * @return the result **/ - private String buildCommand(int x, int y, BuildingType buildingType){ + private String buildCommand(int x, int y, BuildingType buildingType) { return String.format("%s,%d,%s", String.valueOf(x), y, buildingType.getCommandCode()); } /** * Get all buildings for player in row y + * * @param playerType the player type - * @param filter the filter - * @param y the y + * @param filter the filter + * @param y the y * @return the result - * **/ - private List getAllBuildingsForPlayer(PlayerType playerType, Predicate filter, int y){ + **/ + private List getAllBuildingsForPlayer(PlayerType playerType, Predicate filter, int y) { return gameState.getGameMap().stream() .filter(c -> c.cellOwner == playerType && c.y == y) .flatMap(c -> c.getBuildings().stream()) @@ -173,10 +182,11 @@ private List getAllBuildingsForPlayer(PlayerType playerType, Predicate /** * Get all empty cells for column x + * * @param x the x * @return the result - * **/ - private List getListOfEmptyCellsForColumn(int x){ + **/ + private List getListOfEmptyCellsForColumn(int x) { return gameState.getGameMap().stream() .filter(c -> c.x == x && isCellEmpty(x, c.y)) .collect(Collectors.toList()); @@ -184,19 +194,20 @@ private List getListOfEmptyCellsForColumn(int x){ /** * Checks if cell at x,y is empty + * * @param x the x * @param y the y * @return the result - * **/ + **/ private boolean isCellEmpty(int x, int y) { Optional cellOptional = gameState.getGameMap().stream() .filter(c -> c.x == x && c.y == y) .findFirst(); - if (cellOptional.isPresent()){ + if (cellOptional.isPresent()) { CellStateContainer cell = cellOptional.get(); return cell.getBuildings().size() <= 0; - }else{ + } else { System.out.println("Invalid cell selected"); } return true; @@ -204,19 +215,21 @@ private boolean isCellEmpty(int x, int y) { /** * Checks if building can be afforded + * * @param buildingType the building type * @return the result - * **/ - private boolean canAffordBuilding(BuildingType buildingType){ + **/ + private boolean canAffordBuilding(BuildingType buildingType) { return getEnergy(PlayerType.A) >= getPriceForBuilding(buildingType); } /** * Gets energy for player type + * * @param playerType the player type * @return the result - * **/ - private int getEnergy(PlayerType playerType){ + **/ + private int getEnergy(PlayerType playerType) { return gameState.getPlayers().stream() .filter(p -> p.playerType == playerType) .mapToInt(p -> p.energy) @@ -225,27 +238,24 @@ private int getEnergy(PlayerType playerType){ /** * Gets price for building type + * * @param buildingType the player type * @return the result - * **/ - private int getPriceForBuilding(BuildingType buildingType){ - return gameState.gameDetails.buildingPrices.get(buildingType); + **/ + private int getPriceForBuilding(BuildingType buildingType) { + return gameState.gameDetails.buildingsStats.get(buildingType).price; } /** * Gets price for most expensive building type + * * @return the result - * **/ - private int getMostExpensiveBuildingPrice(){ - int buildingPrice = 0; - for (Integer value : gameState.gameDetails.buildingPrices.values()){ - if (buildingPrice == 0){ - buildingPrice = value; - } - if (value > buildingPrice){ - buildingPrice = value; - } - } - return buildingPrice; + **/ + private int getMostExpensiveBuildingPrice() { + return gameState.gameDetails.buildingsStats + .values().stream() + .mapToInt(b -> b.price) + .max() + .orElse(0); } } diff --git a/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java b/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java index adae0a2..2e04874 100644 --- a/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java +++ b/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java @@ -3,7 +3,7 @@ import com.google.gson.Gson; import za.co.entelect.challenge.entities.GameState; -import java.io.*; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; @@ -13,13 +13,14 @@ public class Main { /** * Read the current state, feed it to the bot, get the output and write it to the command. + * * @param args the args **/ public static void main(String[] args) { String state = null; try { state = new String(Files.readAllBytes(Paths.get(STATE_FILE_NAME))); - }catch (IOException e){ + } catch (IOException e) { e.printStackTrace(); } @@ -34,6 +35,7 @@ public static void main(String[] args) { /** * Write bot response to file + * * @param command the command **/ private static void writeBotResponseToFile(String command) { diff --git a/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java b/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java new file mode 100644 index 0000000..298ed11 --- /dev/null +++ b/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java @@ -0,0 +1,15 @@ +package za.co.entelect.challenge.entities; + +public class BuildingStats { + + public int health; + public int constructionTime; + public int price; + public int weaponDamage; + public int weaponSpeed; + public int weaponCooldownPeriod; + public int energyGeneratedPerTurn; + public int destroyMultiplier; + public int constructionScore; + +} diff --git a/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java b/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java index 187491f..019e6a4 100644 --- a/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java +++ b/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java @@ -5,9 +5,12 @@ import java.util.HashMap; public class GameDetails { + public int round; public int mapWidth; public int mapHeight; - public HashMap buildingPrices; + public int roundIncomeEnergy; + public HashMap buildingsStats = new HashMap<>(); + } diff --git a/starter-bots/cplusplus/samplebot.exe b/starter-bots/cplusplus/samplebot.exe new file mode 100644 index 0000000..70522e3 Binary files /dev/null and b/starter-bots/cplusplus/samplebot.exe differ diff --git a/starter-bots/csharpcore/StarterBot/Bot.cs b/starter-bots/csharpcore/StarterBot/Bot.cs index 883411b..deb7073 100644 --- a/starter-bots/csharpcore/StarterBot/Bot.cs +++ b/starter-bots/csharpcore/StarterBot/Bot.cs @@ -9,9 +9,11 @@ namespace StarterBot public class Bot { private readonly GameState _gameState; - private readonly int _attackCost; - private readonly int _defenseCost; - private readonly int _energyCost; + + private readonly BuildingStats _attackStats; + private readonly BuildingStats _defenseStats; + private readonly BuildingStats _energyStats; + private readonly int _mapWidth; private readonly int _mapHeight; private readonly Player _player; @@ -22,9 +24,11 @@ public Bot(GameState gameState) this._gameState = gameState; this._mapHeight = gameState.GameDetails.MapHeight; this._mapWidth = gameState.GameDetails.MapWidth; - this._attackCost = gameState.GameDetails.BuildingPrices[BuildingType.Attack]; - this._defenseCost = gameState.GameDetails.BuildingPrices[BuildingType.Defense]; - this._energyCost = gameState.GameDetails.BuildingPrices[BuildingType.Energy]; + + this._attackStats = gameState.GameDetails.BuildingsStats[BuildingType.Attack]; + this._defenseStats = gameState.GameDetails.BuildingsStats[BuildingType.Defense]; + this._energyStats = gameState.GameDetails.BuildingsStats[BuildingType.Energy]; + this._random = new Random((int) DateTime.Now.Ticks); _player = gameState.Players.Single(x => x.PlayerType == PlayerType.A); @@ -35,7 +39,7 @@ public string Run() var commandToReturn = ""; //This will check if there is enough energy to build any building before processing any commands - if (_player.Energy < _defenseCost && _player.Energy < _energyCost && _player.Energy < _attackCost) + if (_player.Energy < _defenseStats.Price || _player.Energy < _energyStats.Price || _player.Energy < _attackStats.Price) { return commandToReturn; } diff --git a/starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs b/starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs new file mode 100644 index 0000000..62dedb4 --- /dev/null +++ b/starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace StarterBot.Entities +{ + public class BuildingStats + { + public int Health; + public int ConstructionTime; + public int Price; + + //Weapon details, applicable only to attack buildings + public int WeaponDamage; + public int WeaponSpeed; + public int WeaponCooldownPeriod; + + // Energy generation details, only applicable to energy buildings + public int EnergyGeneratedPerTurn; + + // Score details + public int DestroyMultiplier; + public int ConstructionScore; + } +} diff --git a/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs b/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs index 447374b..b8c9169 100644 --- a/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs +++ b/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using StarterBot.Enums; namespace StarterBot.Entities @@ -6,8 +7,8 @@ namespace StarterBot.Entities public class GameDetails { public int Round { get; set; } - public int MapWidth { get; set; } - public int MapHeight { get; set; } - public Dictionary BuildingPrices { get; set; } + public int MapWidth { get; set; } + public int MapHeight { get; set; } + public Dictionary BuildingsStats { get; set; } } } \ No newline at end of file diff --git a/starter-bots/golang/README.md b/starter-bots/golang/README.md new file mode 100644 index 0000000..d7158f4 --- /dev/null +++ b/starter-bots/golang/README.md @@ -0,0 +1,17 @@ +# Go Sample Bot + +A naive and hacky version of a bot in Go. + +## Go runtime + +Find the relevant Go installation files here: https://golang.org/dl/. + +To find out more about the Go language, visit the [project website](https://golang.org). + +## Running + +The game runner will combine compile and execute using the `run` command, rather than as separate steps. For example: + +``` +go run golangbot.go +``` diff --git a/starter-bots/golang/bot.json b/starter-bots/golang/bot.json new file mode 100644 index 0000000..87143f2 --- /dev/null +++ b/starter-bots/golang/bot.json @@ -0,0 +1,8 @@ +{ + "author":"John Doe", + "email":"john.doe@example.com", + "nickName" :"Bob", + "botLocation": "/", + "botFileName": "starterbot.go", + "botLanguage": "golang" +} \ No newline at end of file diff --git a/starter-bots/golang/starterbot.go b/starter-bots/golang/starterbot.go new file mode 100644 index 0000000..11f3f73 --- /dev/null +++ b/starter-bots/golang/starterbot.go @@ -0,0 +1,233 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "time" +) + +const ( + Defense string = "DEFENSE" + Attack string = "ATTACK" + Energy string = "ENERGY" +) + +type Coord struct { + X int + Y int +} + +type BuildingPrices struct { + Defense int `json:"DEFENSE"` + Attack int `json:"ATTACK"` + Energy int `json:"ENERGY"` +} + +var buildingPrice = map[string]int{ + "DEFENSE": 0, + "ATTACK": 0, + "ENERGY": 0, +} + +var buildingCommandVal = map[string]int{ + "DEFENSE": 0, + "ATTACK": 1, + "ENERGY": 2, +} + +type GameDetails struct { + Round int `json:"round"` + MapWidth int `json:"mapWidth"` + MapHeight int `json:"mapHeight"` + BuildingPrices `json:"buildingPrices"` +} + +type Player struct { + PlayerType string `json:"playerType"` + Energy int `json:"energy"` + Health int `json:"health"` +} + +type Building struct { + X int `json:"x"` + Y int `json:"y"` + Health int `json:"health"` + PlayerType string `json:"playerType"` +} + +type Missile struct { + X int `json:"x"` + Y int `json:"y"` + PlayerType string `json:"playerType"` +} + +type Cell struct { + X int `json:"x"` + Y int `json:"y"` + Buildings []Building `json:"buildings"` + Missiles []Missile `json:"missiles"` + CellOwner string `json:"cellOwner"` +} + +type GameState struct { + GameDetails `json:"gameDetails"` + Players []Player `json:"players"` + GameMap [][]Cell `json:"gameMap"` +} + +const stateFilename = "state.json" +const commandFilename = "command.txt" + +var command string +var gameState GameState +var gameDetails GameDetails +var myself Player +var opponent Player +var gameMap [][]Cell +var missiles []Missile +var buildings []Building + +func main() { + runGameCycle() + writeCommand() +} + +func writeCommand() { + err := ioutil.WriteFile(commandFilename, []byte(command), 0666) + if err != nil { + panic(err) + } +} + +func init() { + rand.Seed(time.Now().Unix()) + + data, err := ioutil.ReadFile(stateFilename) + if err != nil { + panic(err.Error()) + } + + var gameState GameState + err = json.Unmarshal(data, &gameState) + if err != nil { + panic(err.Error()) + } + + // load some convenience variables + gameDetails = gameState.GameDetails + gameMap = gameState.GameMap + buildingPrice[Attack] = gameDetails.BuildingPrices.Attack + buildingPrice[Defense] = gameDetails.BuildingPrices.Defense + buildingPrice[Energy] = gameDetails.BuildingPrices.Energy + + for _, player := range gameState.Players { + switch player.PlayerType { + case "A": + myself = player + case "B": + opponent = player + } + } + + for x := 0; x < gameDetails.MapHeight; x++ { + for y := 0; y < gameDetails.MapWidth; y++ { + cell := gameMap[x][y] + for missileIndex := 0; missileIndex < len(cell.Missiles); missileIndex++ { + missiles = append(missiles, cell.Missiles[missileIndex]) + } + for buildingIndex := 0; buildingIndex < len(cell.Buildings); buildingIndex++ { + buildings = append(buildings, cell.Buildings[buildingIndex]) + } + } + } +} + +func runGameCycle() { + var row int + var coord = Coord{-1, -1} + + if underAttack(&row) && canBuild(Defense) { + coord = chooseLocationToDefend(row) + buildBuilding(Defense, coord) + } else if canBuild(Attack) { + buildBuilding(Attack, coord) + } else { + doNothing() + } +} + +func underAttack(row *int) bool { + *row = -1 + for _, missile := range missiles { + if missile.PlayerType == opponent.PlayerType { + *row = missile.Y + break + } + } + return *row >= 0 +} + +func chooseLocationToDefend(row int) Coord { + var col = 0 + for _, building := range buildings { + if building.PlayerType == myself.PlayerType && building.Y == row { + if building.X > col { + col = building.X + } + } + } + if col >= (gameDetails.MapWidth/2)-1 { + return randomUnoccupiedCoordinate() + } + + return Coord{X: col + 1, Y: row} +} + +func canBuild(buildingType string) bool { + return myself.Energy >= buildingPrice[buildingType] +} + +func buildBuilding(buildingType string, coord Coord) { + if coord.X < 0 || coord.Y < 0 { + coord = randomUnoccupiedCoordinate() + } + command = fmt.Sprintf("%d,%d,%d", coord.X, coord.Y, buildingCommandVal[buildingType]) +} + +func doNothing() { + command = "" +} + +func randomCoordinate() Coord { + var coord = Coord{} + coord.X = rand.Intn(gameDetails.MapWidth / 2) + coord.Y = rand.Intn(gameDetails.MapHeight) + return coord +} + +func randomUnoccupiedCoordinate() Coord { + var coord Coord + + for { + coord = randomCoordinate() + if isOccupied(coord) == false { + break + } + } + return coord +} + +func isOccupied(coord Coord) bool { + if coord.X < 0 || coord.X >= gameDetails.MapWidth || coord.Y < 0 || coord.Y >= gameDetails.MapHeight { + return false + } + var cell = gameMap[coord.X][coord.Y] + return len(cell.Buildings) != 0 +} + +func prettyPrint(v interface{}) { + b, _ := json.MarshalIndent(v, "", " ") + println(string(b)) +} diff --git a/starter-bots/haskell/.gitignore b/starter-bots/haskell/.gitignore new file mode 100644 index 0000000..ad04ed9 --- /dev/null +++ b/starter-bots/haskell/.gitignore @@ -0,0 +1,15 @@ +# Editor files +*~ +TAGS + +# Project (stack) files +.stack-work/ +EntelectChallenge2018.cabal + +# Compiled files +*.o +bin/ + +# Game files +command.txt +state.json \ No newline at end of file diff --git a/starter-bots/haskell/ChangeLog.md b/starter-bots/haskell/ChangeLog.md new file mode 100644 index 0000000..0ac05d8 --- /dev/null +++ b/starter-bots/haskell/ChangeLog.md @@ -0,0 +1,3 @@ +# Changelog for EntelectChallenge2018 + +## Unreleased changes diff --git a/starter-bots/haskell/LICENSE b/starter-bots/haskell/LICENSE new file mode 100644 index 0000000..67fcee8 --- /dev/null +++ b/starter-bots/haskell/LICENSE @@ -0,0 +1,14 @@ +Copyright Edward John Steere (c) 2018 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/starter-bots/haskell/README.md b/starter-bots/haskell/README.md new file mode 100644 index 0000000..d50ec75 --- /dev/null +++ b/starter-bots/haskell/README.md @@ -0,0 +1,26 @@ +# Haskell Sample Bot +Haskell is a purely functional programming language. You can find out +more about Haskell [here](https://www.haskell.org/). + +## Environment Requirements +Install the [Haskell Platform](https://www.haskell.org/platform/) and +ensure that the `stack` executable is on the path. + +## Building +Simply run: + +``` +stack install --local-bin-path bin +``` + +to build the binary and put it into a folder in the root of the +project called `bin`. + +## Running +Haskell creates native binaries so you can simply run: + +``` +./bin/EntelectChallenge2018-exe +``` + +from the command line to invoke the bot program. diff --git a/starter-bots/haskell/Setup.hs b/starter-bots/haskell/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/starter-bots/haskell/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/starter-bots/haskell/app/Main.hs b/starter-bots/haskell/app/Main.hs new file mode 100644 index 0000000..46b4d15 --- /dev/null +++ b/starter-bots/haskell/app/Main.hs @@ -0,0 +1,10 @@ +module Main where + +import Interpretor (repl) +import Bot (decide) +import System.Random + +main :: IO () +main = do + gen <- getStdGen + repl (decide gen) diff --git a/starter-bots/haskell/bot.json b/starter-bots/haskell/bot.json new file mode 100644 index 0000000..ed3c743 --- /dev/null +++ b/starter-bots/haskell/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "Bill", + "botLocation": "/bin", + "botFileName": "EntelectChallenge2018-exe", + "botLanguage": "haskell" +} diff --git a/starter-bots/haskell/package.yaml b/starter-bots/haskell/package.yaml new file mode 100644 index 0000000..f1e0960 --- /dev/null +++ b/starter-bots/haskell/package.yaml @@ -0,0 +1,54 @@ +name: EntelectChallenge2018 +version: 0.1.0.0 +github: "quiescent/EntelectChallenge2018" +license: GPL-3 +author: "Edward John Steere" +maintainer: "edward.steere@gmail.com" +copyright: "2018 Edward John Steere" + +extra-source-files: +- README.md +- ChangeLog.md + +# Metadata used when publishing your package +# synopsis: Short description of your package +# category: Web + +# To avoid duplicated efforts in documentation and dealing with the +# complications of embedding Haddock markup inside cabal files, it is +# common to point users to the README.md file. +description: Please see the README on GitHub at + +dependencies: +- base >= 4.7 && < 5 +- aeson >= 1.2.4.0 +- containers >= 0.5.10.0 +- vector >= 0.12.0.1 +- random >= 1.1 +- bytestring >= 0.10.8.2 + +library: + source-dirs: src + +executables: + EntelectChallenge2018-exe: + main: Main.hs + source-dirs: app + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - EntelectChallenge2018 + +tests: + EntelectChallenge2018-test: + main: Spec.hs + source-dirs: test + buildable: false + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - EntelectChallenge2018 diff --git a/starter-bots/haskell/src/Bot.hs b/starter-bots/haskell/src/Bot.hs new file mode 100644 index 0000000..550db49 --- /dev/null +++ b/starter-bots/haskell/src/Bot.hs @@ -0,0 +1,122 @@ +module Bot + where + +import Interpretor (GameState(..), + Command, + GameDetails(..), + Building(..), + CellStateContainer(..), + PlayerType(..), + BuildingType(..), + BuildingPriceIndex(..), + Player(..)) +import Data.List +import System.Random +import Control.Monad + +-- Predicate combination operator +(&&&) :: (a -> Bool) -> (a -> Bool) -> (a -> Bool) +(&&&) f g = \ input -> f input && g input + +cellBelongsTo :: PlayerType -> CellStateContainer -> Bool +cellBelongsTo typeOfPlayer = + (==typeOfPlayer) . cellOwner + +cellContainsBuildingType :: BuildingType -> CellStateContainer -> Bool +cellContainsBuildingType typeOfBuilding = + any ((==typeOfBuilding) . buildingType) . buildings + +enemyHasAttacking :: GameState -> Int -> Bool +enemyHasAttacking state = + any cellContainsEnemyAttacker . ((gameMap state) !!) + where + cellContainsEnemyAttacker = + (cellBelongsTo B) &&& (cellContainsBuildingType ATTACK) + +cellBelongsToMe :: CellStateContainer -> Bool +cellBelongsToMe = cellBelongsTo A + +iDontHaveDefense :: GameState -> Int -> Bool +iDontHaveDefense state = + not . any cellContainDefenseFromMe . ((gameMap state) !!) + where + cellContainDefenseFromMe = + cellBelongsToMe &&& (cellContainsBuildingType DEFENSE) + +thereIsAnEmptyCellInRow :: GameState -> Int -> Bool +thereIsAnEmptyCellInRow (GameState {gameMap = gameMap'})= + any cellIsEmpty . (gameMap' !!) + +indexOfFirstEmpty :: GameState -> Int -> Maybe Int +indexOfFirstEmpty (GameState {gameMap = gameMap'}) = + fmap yPos . find (cellIsEmpty &&& cellBelongsToMe) . (gameMap' !!) + +defendAttack :: GameState -> Maybe (Int, Int, BuildingType) +defendAttack state@(GameState _ _ (GameDetails _ _ height _)) = do + x <- find rowUnderAttack [0..height - 1] + y <- indexOfFirstEmpty state x + return (x, y, DEFENSE) + where + rowUnderAttack = (enemyHasAttacking state) &&& + (iDontHaveDefense state) &&& + (thereIsAnEmptyCellInRow state) + +hasEnoughEnergyForMostExpensiveBuilding :: GameState -> Bool +hasEnoughEnergyForMostExpensiveBuilding state@(GameState _ _ (GameDetails { buildingPrices = prices })) = + ourEnergy >= maxPrice + where + ourEnergy = energy ourPlayer + ourPlayer = (head . filter ((==A) . playerType) . players) state + maxPrice = maximum towerPrices + towerPrices = map ($ prices) [attackTowerCost, defenseTowerCost, energyTowerCost] + +cellIsEmpty :: CellStateContainer -> Bool +cellIsEmpty = ([] ==) . buildings + +myEmptyCells :: [[CellStateContainer]] -> [CellStateContainer] +myEmptyCells = + concat . map (filter isMineAndIsEmpty) + where + isMineAndIsEmpty = cellIsEmpty &&& cellBelongsToMe + +randomEmptyCell :: RandomGen g => g -> GameState -> ((Int, Int), g) +randomEmptyCell gen (GameState {gameMap = mapGrid}) = + let emptyCells = myEmptyCells mapGrid + (randomInt, newGenerator) = next gen + emptyCell = emptyCells !! mod randomInt (length emptyCells) + in ((xPos emptyCell, yPos emptyCell), newGenerator) + +randomBuilding :: RandomGen g => g -> (BuildingType, g) +randomBuilding gen = + let (randomInt, gen') = next gen + buildingIndex = mod randomInt 3 + in (case buildingIndex of + 0 -> DEFENSE + 1 -> ATTACK + _ -> ENERGY, + gen') + +buildRandomly :: RandomGen g => g -> GameState -> Maybe (Int, Int, BuildingType) +buildRandomly gen state = + if not $ hasEnoughEnergyForMostExpensiveBuilding state + then Nothing + else let ((x, y), gen') = randomEmptyCell gen state + (building, _) = randomBuilding gen' + in Just (x, y, building) + +doNothingCommand :: Command +doNothingCommand = "" + +build :: Int -> Int -> BuildingType -> Command +build x y buildingType' = + show x ++ "," ++ show y ++ "," ++ + case buildingType' of + DEFENSE -> "0" + ATTACK -> "1" + ENERGY -> "2" + +decide :: RandomGen g => g -> GameState -> Command +decide gen state = + case msum [defendAttack state, buildRandomly gen state] of + Just (x, y, building) -> build x y building + Nothing -> doNothingCommand diff --git a/starter-bots/haskell/src/Interpretor.hs b/starter-bots/haskell/src/Interpretor.hs new file mode 100644 index 0000000..09b410f --- /dev/null +++ b/starter-bots/haskell/src/Interpretor.hs @@ -0,0 +1,223 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE FlexibleInstances #-} + +module Interpretor (repl, + Player(..), + PlayerType(..), + Missile(..), + Cell(..), + BuildingType(..), + Building(..), + CellStateContainer(..), + BuildingPriceIndex(..), + GameDetails(..), + GameState(..), + Command) + where + +import Data.Aeson (decode, + FromJSON, + parseJSON, + withObject, + (.:), + ToJSON, + toJSON, + object, + (.=)) +import Data.Vector as V +import GHC.Generics (Generic) +import Data.ByteString.Lazy as B + +data PlayerType = + A | B deriving (Show, Generic, Eq) + +instance FromJSON PlayerType +instance ToJSON PlayerType + +data Player = Player { playerType :: PlayerType, + energy :: Int, + health :: Int, + hitsTaken :: Int, + score :: Int } + deriving (Show, Generic, Eq) + +instance FromJSON Player +instance ToJSON Player + +data Missile = Missile { damage :: Int, speed :: Int } + deriving (Show, Generic, Eq) + +instance FromJSON Missile +instance ToJSON Missile + +data Cell = Cell { x :: Int, y :: Int, owner :: PlayerType } + deriving (Show, Generic, Eq) + +instance FromJSON Cell +instance ToJSON Cell + +data BuildingType = DEFENSE | ATTACK | ENERGY + deriving (Show, Generic, Eq) + +instance FromJSON BuildingType +instance ToJSON BuildingType + +data Building = Building { integrity :: Int, + constructionTimeLeft :: Int, + price :: Int, + weaponDamage :: Int, + weaponSpeed :: Int, + weaponCooldownTimeLeft :: Int, + weaponCooldownPeriod :: Int, + destroyMultiplier :: Int, + constructionScore :: Int, + energyGeneratedPerTurn :: Int, + buildingType :: BuildingType, + buildingX :: Int, + buildingY :: Int, + buildingOwner :: PlayerType } + deriving (Show, Generic, Eq) + +instance FromJSON Building where + parseJSON = withObject "Building" $ \ v -> + Building <$> v .: "health" + <*> v .: "constructionTimeLeft" + <*> v .: "price" + <*> v .: "weaponDamage" + <*> v .: "weaponSpeed" + <*> v .: "weaponCooldownTimeLeft" + <*> v .: "weaponCooldownPeriod" + <*> v .: "destroyMultiplier" + <*> v .: "constructionScore" + <*> v .: "energyGeneratedPerTurn" + <*> v .: "buildingType" + <*> v .: "x" + <*> v .: "y" + <*> v .: "playerType" +instance ToJSON Building where + toJSON (Building integrity' + constructionTimeLeft' + price' + weaponDamage' + weaponSpeed' + weaponCooldownTimeLeft' + weaponCooldownPeriod' + destroyMultiplier' + constructionScore' + energyGeneratedPerTurn' + buildingType' + buildingX' + buildingY' + buildingOwner') = + object ["health" .= integrity', + "constructionTimeLeft" .= constructionTimeLeft', + "price" .= price', + "weaponDamage" .= weaponDamage', + "weaponSpeed" .= weaponSpeed', + "weaponCooldownTimeLeft" .= weaponCooldownTimeLeft', + "weaponCooldownPeriod" .= weaponCooldownPeriod', + "destroyMultiplier" .= destroyMultiplier', + "constructionScore" .= constructionScore', + "energyGeneratedPerTurn" .= energyGeneratedPerTurn', + "buildingType" .= buildingType', + "x" .= buildingX', + "y" .= buildingY', + "playerType" .= buildingOwner'] + +data CellStateContainer = CellStateContainer { xPos :: Int, + yPos :: Int, + cellOwner :: PlayerType, + buildings :: [Building], + missiles :: [Missile] } + deriving (Show, Generic, Eq) + +instance FromJSON CellStateContainer where + parseJSON = withObject "CellStateContainer" $ \ v -> do + x' <- v .: "x" + y' <- v .: "y" + cellOwner' <- v .: "cellOwner" + buildings' <- v .: "buildings" + buildings'' <- Prelude.mapM parseJSON $ V.toList buildings' + missiles' <- v .: "missiles" + missiles'' <- Prelude.mapM parseJSON $ V.toList missiles' + return $ CellStateContainer x' + y' + cellOwner' + buildings'' + missiles'' + +instance ToJSON CellStateContainer where + toJSON (CellStateContainer xPos' + yPos' + cellOwner' + buildings' + missiles') = + object ["x" .= xPos', + "y" .= yPos', + "cellOwner" .= cellOwner', + "buildings" .= buildings', + "missiles" .= missiles'] + +data BuildingPriceIndex = BuildingPriceIndex { attackTowerCost :: Int, + defenseTowerCost :: Int, + energyTowerCost :: Int } + deriving (Show, Generic, Eq) + +instance FromJSON BuildingPriceIndex where + parseJSON = withObject "BuildingPriceIndex" $ \ v -> + BuildingPriceIndex <$> v .: "ATTACK" + <*> v .: "DEFENSE" + <*> v .: "ENERGY" +instance ToJSON BuildingPriceIndex where + toJSON (BuildingPriceIndex attackCost defenseCost energyCost) = + object ["ATTACK" .= attackCost, + "DEFENSE" .= defenseCost, + "ENERGY" .= energyCost] + +data GameDetails = GameDetails { round :: Int, + mapWidth :: Int, + mapHeight :: Int, + buildingPrices :: BuildingPriceIndex } + deriving (Show, Generic, Eq) + +instance FromJSON GameDetails +instance ToJSON GameDetails + +data GameState = GameState { players :: [Player], + gameMap :: [[CellStateContainer]], + gameDetails :: GameDetails } + deriving (Show, Generic, Eq) + +instance FromJSON GameState where + parseJSON = withObject "GameState" $ \ v -> do + playersProp <- v .: "players" + playersList <- Prelude.mapM parseJSON $ V.toList playersProp + gameMapObject <- v .: "gameMap" + gameMapProp <- Prelude.mapM parseJSON $ V.toList gameMapObject + gameDetailsProp <- v .: "gameDetails" + return $ GameState playersList gameMapProp gameDetailsProp + +instance ToJSON GameState where + toJSON (GameState gamePlayers mapForGame details) = + object ["players" .= gamePlayers, "gameMap" .= mapForGame, "gameDetails" .= details] + +stateFilePath :: String +stateFilePath = "state.json" + +commandFilePath :: String +commandFilePath = "command.txt" + +readGameState :: IO GameState +readGameState = do + stateString <- B.readFile stateFilePath + let Just state = decode stateString + return state + +printGameState :: String -> IO () +printGameState command = Prelude.writeFile commandFilePath command + +type Command = String + +repl :: (GameState -> Command) -> IO () +repl evaluate = fmap evaluate readGameState >>= printGameState diff --git a/starter-bots/haskell/stack.yaml b/starter-bots/haskell/stack.yaml new file mode 100644 index 0000000..eb506f9 --- /dev/null +++ b/starter-bots/haskell/stack.yaml @@ -0,0 +1,66 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# resolver: ghcjs-0.1.0_ghc-7.10.2 +# resolver: +# name: custom-snapshot +# location: "./custom-snapshot.yaml" +resolver: lts-11.7 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# extra-dep: true +# subdirs: +# - auto-update +# - wai +# +# A package marked 'extra-dep: true' will only be built if demanded by a +# non-dependency (i.e. a user package), and its test suites and benchmarks +# will not be run. This is useful for tweaking upstream packages. +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# (e.g., acme-missiles-0.3) +# extra-deps: [] + +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.6" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor \ No newline at end of file diff --git a/starter-bots/haskell/test/Spec.hs b/starter-bots/haskell/test/Spec.hs new file mode 100644 index 0000000..cd4753f --- /dev/null +++ b/starter-bots/haskell/test/Spec.hs @@ -0,0 +1,2 @@ +main :: IO () +main = putStrLn "Test suite not yet implemented" diff --git a/starter-bots/java/pom.xml b/starter-bots/java/pom.xml index eb5be9e..915ef79 100644 --- a/starter-bots/java/pom.xml +++ b/starter-bots/java/pom.xml @@ -6,7 +6,7 @@ za.co.entelect.challenge java-sample-bot - 1.0-SNAPSHOT + 1.1-SNAPSHOT diff --git a/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java b/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java index 772b711..465afd6 100644 --- a/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java +++ b/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java @@ -91,9 +91,10 @@ private String buildRandom() { * @return the result **/ private boolean hasEnoughEnergyForMostExpensiveBuilding() { - return gameDetails.buildingPrices.values().stream() - .filter(bp -> bp < myself.energy) - .toArray().length == 3; + return gameDetails.buildingsStats.values().stream() + .filter(b -> b.price <= myself.energy) + .toArray() + .length == 3; } /** @@ -185,6 +186,6 @@ private boolean getAnyBuildingsForPlayer(PlayerType playerType, Predicate= gameDetails.buildingPrices.get(buildingType); + return myself.energy >= gameDetails.buildingsStats.get(buildingType).price; } } diff --git a/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java b/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java new file mode 100644 index 0000000..298ed11 --- /dev/null +++ b/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java @@ -0,0 +1,15 @@ +package za.co.entelect.challenge.entities; + +public class BuildingStats { + + public int health; + public int constructionTime; + public int price; + public int weaponDamage; + public int weaponSpeed; + public int weaponCooldownPeriod; + public int energyGeneratedPerTurn; + public int destroyMultiplier; + public int constructionScore; + +} diff --git a/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java b/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java index 565682d..68a56e7 100644 --- a/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java +++ b/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java @@ -8,6 +8,7 @@ public class GameDetails { public int round; public int mapWidth; public int mapHeight; - public HashMap buildingPrices; + public int roundIncomeEnergy; + public HashMap buildingsStats = new HashMap<>(); } diff --git a/starter-bots/javascript/StarterBot.js b/starter-bots/javascript/StarterBot.js index c142342..d71130e 100644 --- a/starter-bots/javascript/StarterBot.js +++ b/starter-bots/javascript/StarterBot.js @@ -15,7 +15,7 @@ let mapSize = ""; let cells = ""; let buildings = ""; let missiles = ""; -let buildingPrices = []; +let buildingStats = []; // Capture the arguments initBot(process.argv.slice(2)); @@ -34,10 +34,10 @@ function initBot(args) { y: stateFile.gameDetails.mapHeight }; - let prices = stateFile.gameDetails.buildingPrices; - buildingPrices[0]= prices.DEFENSE; - buildingPrices[1]= prices.ATTACK; - buildingPrices[2]= prices.ENERGY; + let stats = stateFile.gameDetails.buildingsStats; + buildingStats[0]= stats.DEFENSE; + buildingStats[1]= stats.ATTACK; + buildingStats[2]= stats.ENERGY; gameMap = stateFile.gameMap; initEntities(); @@ -71,7 +71,7 @@ function isUnderAttack() { let opponentAttackers = buildings.filter(b => b.playerType == 'B' && b.buildingType == 'ATTACK') .filter(b => !myDefenders.some(d => d.y == b.y)); - return (opponentAttackers.length > 0) && (myself.energy >= buildingPrices[0]); + return (opponentAttackers.length > 0) && (myself.energy >= buildingStats[0].price); } function defendRow() { @@ -101,7 +101,7 @@ function defendRow() { } function hasEnoughEnergyForMostExpensiveBuilding() { - return (myself.energy >= Math.max(...buildingPrices)); + return (myself.energy >= Math.max(...(buildingStats.map(stat => stat.price)))); } function buildRandom() { diff --git a/starter-bots/php/README.md b/starter-bots/php/README.md new file mode 100644 index 0000000..7c0ab88 --- /dev/null +++ b/starter-bots/php/README.md @@ -0,0 +1,14 @@ +# PHP Starter Bot + +PHP is a popular general-purpose scripting language that is especially suited to web development. + +Fast, flexible and pragmatic, PHP powers everything from your blog to the most popular websites in the world. + + +## Environment Setup + +The bot requires PHP 7.0 or greater. + +For instructions on installing PHP for your OS, please see the documentation at [http://php.net/manual/en/install.php](http://php.net/manual/en/install.php). + +Also be sure that the PHP CLI binary is in your OS's PATH environment variable. diff --git a/starter-bots/php/StarterBot.php b/starter-bots/php/StarterBot.php new file mode 100644 index 0000000..d4b1cbf --- /dev/null +++ b/starter-bots/php/StarterBot.php @@ -0,0 +1,16 @@ +decideAction()); + +fclose($outputFile); diff --git a/starter-bots/php/bot.json b/starter-bots/php/bot.json new file mode 100644 index 0000000..cfbd92a --- /dev/null +++ b/starter-bots/php/bot.json @@ -0,0 +1,8 @@ +{ + "author":"John Doe", + "email":"john.doe@example.com", + "nickName" :"Engelbert", + "botLocation": "/", + "botFileName": "StarterBot.php", + "botLanguage": "php" +} diff --git a/starter-bots/php/include/Bot.php b/starter-bots/php/include/Bot.php new file mode 100644 index 0000000..f36c9ee --- /dev/null +++ b/starter-bots/php/include/Bot.php @@ -0,0 +1,79 @@ +_game = $state; + $this->_map = $this->_game->getMap(); + } + + /** + * This is the main function for deciding which action to take + * + * Returns a valid action string + */ + public function decideAction() + { + //Check if we should defend + list($x,$y,$building) = $this->checkDefense(); + + //If no defend orders then build randomly + list($x,$y,$building) = $x === null ? $this->buildRandom() : [$x, $y, $building]; + + if ($x !== null && $this->_game->getBuildingPrice($building) <= $this->_game->getPlayerA()->energy) + { + return "$x,$y,$building"; + } + return ""; + } + + /** + * Checks if a row is being attacked and returns a build order if there is an empty space + * and no defensive buildings in the that row. + */ + protected function checkDefense() + { + for ($row = 0; $row < $this->_game->getMapHeight(); $row++) + { + if ($this->_map->isAttackedRow($row) && !$this->_map->rowHasOwnDefense($row)) + { + list($x,$y,$building) = $this->buildDefense($row); + if ($x !== null) + { + return [$x,$y,$building]; + } + } + } + return [null, null, null]; + } + + /** + * Returns defensive build order at last empty cell in a row + */ + protected function buildDefense($row) + { + //Check for last valid empty cell + $x = $this->_map->getLastEmptyCell($row); + return $x === false ? [$x, $y, Map::DEFENSE] : [null, null, null]; + } + + /** + * Returns a random build order on an empty cell + */ + protected function buildRandom() + { + $emptyCells = $this->_map->getValidBuildCells(); + if (!count($emptyCells)) + { + return [null, null, null]; + } + + $cell = $emptyCells[rand(0,count($emptyCells)-1)]; + $building = rand(0,2); + + return [$cell->x,$cell->y,$building]; + } +} diff --git a/starter-bots/php/include/GameState.php b/starter-bots/php/include/GameState.php new file mode 100644 index 0000000..adf36e3 --- /dev/null +++ b/starter-bots/php/include/GameState.php @@ -0,0 +1,98 @@ +_state = json_decode(file_get_contents($filename)); + $_map = null; + } + + /** + * Returns the entire state object for manual processing + */ + public function getState() + { + return $this->_state; + } + + public function getMapWidth() + { + return $this->_state->gameDetails->mapWidth; + } + + public function getMapHeight() + { + return $this->_state->gameDetails->mapHeight; + } + + public function getPlayerA() + { + foreach ($this->_state->players as $player) + { + if ($player->playerType == "A") + { + return $player; + } + } + } + + public function getPlayerB() + { + foreach ($this->_state->players as $player) + { + if ($player->playerType == "B") + { + return $player; + } + } + } + + /** + * Looks up the price of a particular building type + */ + public function getBuildingPrice(int $type) + { + switch ($type) + { + case Map::DEFENSE: + $str = MAP::DEFENSE_STR; + break; + case Map::ATTACK: + $str = MAP::ATTACK_STR; + break; + case Map::ENERGY: + $str = MAP::ENERGY_STR; + break; + default: + return false; + break; + } + return $this->_state->gameDetails->buildingPrices->$str; + } + + /** + * Returns the current round number + */ + public function getRound() + { + return $this->_state->gameDetails->round(); + } + + /** + * Returns a Map object for examining the playing field + */ + public function getMap() + { + if ($this->_map === null) + { + $this->_map = new Map($this->_state->gameMap); + } + + return $this->_map; + } +} diff --git a/starter-bots/php/include/Map.php b/starter-bots/php/include/Map.php new file mode 100644 index 0000000..876cdba --- /dev/null +++ b/starter-bots/php/include/Map.php @@ -0,0 +1,119 @@ +_map = $map; + } + + /** + * Returns the building at a set of coordinates or false if empty + */ + public function getBuilding($x,$y) + { + return count($this->_map[$y][$x]->buildings) ? $this->_map[$y][$x]->buildings[0] : false; + } + + /** + * Returns the missiles at a set of coordinates or false if no missiles + */ + public function getMissiles($x,$y) + { + return count($this->_map[$y][$x]->missiles) ? $this->_map[$y][$x]->missiles : false; + } + + /** + * Returns the x coordinate of the last empty cell in a row + */ + public function getLastEmptyCell($y) + { + for ($x = count($this->_map[$y])/2 - 1; $x >= 0; $x--) + { + if (!$this->getBuilding($x,$y)) + { + return $x; + } + } + return false; + } + + /** + * Returns the x coordinate of the first empty cell in a row + */ + public function getFirstEmptyCell($y) + { + for ($x = 0; $x < count($this->_map[$y])/2; $x++) + { + if (!$this->getBuilding($x,$y)) + { + return $x; + } + } + return false; + } + + /** + * Returns an array of all valid empty build cells + */ + public function getValidBuildCells() + { + $emptyCells = []; + foreach ($this->_map as $row) + { + foreach ($row as $cell) + { + if ($cell->cellOwner == 'A' && !count($cell->buildings)) + { + $emptyCells[] = $cell; + } + } + } + + return $emptyCells; + } + + /** + * Checks if a row is currently under attack by an enemy + */ + public function isAttackedRow($y) + { + foreach ($this->_map[$y] as $cell) + { + foreach ($cell->missiles as $missile) + { + if ($missile->playerType == 'B') + { + return true; + } + } + } + return false; + } + + /** + * Checks if there is a friendly defensive building in a row + */ + public function rowHasOwnDefense($y) + { + foreach ($this->_map[$y] as $cell) + { + foreach ($cell->buildings as $building) + { + if ($building->buildingType == self::DEFENSE_STR && $building->playerType == 'A') + { + return true; + } + } + } + return false; + } +} diff --git a/starter-bots/python3/StarterBot.py b/starter-bots/python3/StarterBot.py index 4b0e81b..110eef8 100644 --- a/starter-bots/python3/StarterBot.py +++ b/starter-bots/python3/StarterBot.py @@ -40,9 +40,33 @@ def __init__(self,state_location): self.round = self.game_state['gameDetails']['round'] - self.prices = {"ATTACK":self.game_state['gameDetails']['buildingPrices']['ATTACK'], - "DEFENSE":self.game_state['gameDetails']['buildingPrices']['DEFENSE'], - "ENERGY":self.game_state['gameDetails']['buildingPrices']['ENERGY']} + self.buildings_stats = {"ATTACK":{"health": self.game_state['gameDetails']['buildingsStats']['ATTACK']['health'], + "constructionTime": self.game_state['gameDetails']['buildingsStats']['ATTACK']['constructionTime'], + "price": self.game_state['gameDetails']['buildingsStats']['ATTACK']['price'], + "weaponDamage": self.game_state['gameDetails']['buildingsStats']['ATTACK']['weaponDamage'], + "weaponSpeed": self.game_state['gameDetails']['buildingsStats']['ATTACK']['weaponSpeed'], + "weaponCooldownPeriod": self.game_state['gameDetails']['buildingsStats']['ATTACK']['weaponCooldownPeriod'], + "energyGeneratedPerTurn": self.game_state['gameDetails']['buildingsStats']['ATTACK']['energyGeneratedPerTurn'], + "destroyMultiplier": self.game_state['gameDetails']['buildingsStats']['ATTACK']['destroyMultiplier'], + "constructionScore": self.game_state['gameDetails']['buildingsStats']['ATTACK']['constructionScore']}, + "DEFENSE":{"health": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['health'], + "constructionTime": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['constructionTime'], + "price": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['price'], + "weaponDamage": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['weaponDamage'], + "weaponSpeed": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['weaponSpeed'], + "weaponCooldownPeriod": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['weaponCooldownPeriod'], + "energyGeneratedPerTurn": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['energyGeneratedPerTurn'], + "destroyMultiplier": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['destroyMultiplier'], + "constructionScore": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['constructionScore']}, + "ENERGY":{"health": self.game_state['gameDetails']['buildingsStats']['ENERGY']['health'], + "constructionTime": self.game_state['gameDetails']['buildingsStats']['ENERGY']['constructionTime'], + "price": self.game_state['gameDetails']['buildingsStats']['ENERGY']['price'], + "weaponDamage": self.game_state['gameDetails']['buildingsStats']['ENERGY']['weaponDamage'], + "weaponSpeed": self.game_state['gameDetails']['buildingsStats']['ENERGY']['weaponSpeed'], + "weaponCooldownPeriod": self.game_state['gameDetails']['buildingsStats']['ENERGY']['weaponCooldownPeriod'], + "energyGeneratedPerTurn": self.game_state['gameDetails']['buildingsStats']['ENERGY']['energyGeneratedPerTurn'], + "destroyMultiplier": self.game_state['gameDetails']['buildingsStats']['ENERGY']['destroyMultiplier'], + "constructionScore": self.game_state['gameDetails']['buildingsStats']['ENERGY']['constructionScore']}} return None @@ -217,7 +241,7 @@ def generateAction(self): if len(self.getUnOccupied(self.player_buildings[i])) == 0: #cannot place anything in a lane with no available cells. continue - elif ( self.checkAttack(i) and (self.player_info['energy'] >= self.prices['DEFENSE']) and (self.checkMyDefense(i)) == False): + elif ( self.checkAttack(i) and (self.player_info['energy'] >= self.buildings_stats['DEFENSE']['price']) and (self.checkMyDefense(i)) == False): #place defense unit if there is an attack building and you can afford a defense building lanes.append(i) #lanes variable will now contain information about all lanes which have attacking units @@ -230,9 +254,9 @@ def generateAction(self): x = random.choice(self.getUnOccupied(self.player_buildings[i])) #otherwise, build a random building type at a random unoccupied location # if you can afford the most expensive building - elif self.player_info['energy'] >= max(s.prices.values()): + elif self.player_info['energy'] >= max(self.buildings_stats['ATTACK']['price'], self.buildings_stats['DEFENSE']['price'], self.buildings_stats['ENERGY']['price']): building = random.choice([0,1,2]) - x = random.randint(0,self.rows) + x = random.randint(0,self.rows-1) y = random.randint(0,int(self.columns/2)-1) else: self.writeDoNothing() diff --git a/starter-pack/ReadMe.txt b/starter-pack/ReadMe.txt index 5dcdae6..88781f3 100644 --- a/starter-pack/ReadMe.txt +++ b/starter-pack/ReadMe.txt @@ -12,13 +12,13 @@ | |____| | | |/ ____ \| |____| |____| |____| |\ | |__| | |____ \_____|_| |_/_/ \_\______|______|______|_| \_|\_____|______| - __ __ ___ -/_ | /_ | / _ \ - | | | | | | | | - | | | | | | | | - | | _ | | _ | |_| | - |_| (_) |_| (_) \___/ - + __ __ __ +/_ | /_ | /_ | + | | | | | | + | | | | | | + | | _ | | _ | | + |_| (_) |_| (_) |_| + Welcome to the starter pack for the 2018 Entelect Challenge! Here you will find all that you'll need to run your first bot and compete in this year's challenge. @@ -50,6 +50,10 @@ The format of the 'config.json' is as follows: "round-state-output-location" => This is the path to where you want the match folder in which each round's folder with its respective logs will be saved. + "game-config-file-location" => This is the path to the game-config.properties file that is used to set various game-engine settings such as map size and building stats. + + "verbose-mode" => This is a true or false value to either print logs to the console or not respectively. + "max-runtime-ms" => This is the amount of milliseconds that the game runner will allow a bot to run before making its command each round. "player-a" & @@ -81,6 +85,9 @@ Javascript => "javascript" Rust => "rust" C++ => "c++" Kotlin => "kotlin" +Golang => "golang" +Haskell => "haskell" +PHP => "php" @@ -141,3 +148,15 @@ C++ bots Kotlin bots For more info on the Kotlin bot, see the source files or contact the person who submitted the bot, gkmauer (on GitHub) [https://github.com/EntelectChallenge/2018-TowerDefence/tree/master/starter-bots/kotlin] + +Golang bots + For more info on the Golang bot, see the readme file or contact the person who submitted the bot, dougcrawford (on GitHub) + [https://github.com/EntelectChallenge/2018-TowerDefence/tree/master/starter-bots/golang] + +Haskell bots + For more info on the Haskell bot, see the readme file or contact the person who submitted the bot, Quiescent (on GitHub) + [https://github.com/EntelectChallenge/2018-TowerDefence/tree/master/starter-bots/haskell] + +PHP bots + For more info on the PHP bot, see the readme file or contact the person who submitted the bot, PuffyZA (on GitHub) + [https://github.com/EntelectChallenge/2018-TowerDefence/tree/master/starter-bots/php] diff --git a/starter-pack/config.json b/starter-pack/config.json index 242f564..803008b 100644 --- a/starter-pack/config.json +++ b/starter-pack/config.json @@ -1,6 +1,7 @@ { "round-state-output-location": "./tower-defence-matches", + "game-config-file-location": "./game-config.properties", "max-runtime-ms": 2000, - "player-a": "./starter-bots/python2", - "player-b": "./starter-bots/javascript" -} + "player-a": "./starter-bots/java", + "player-b": "./reference-bot/java" +} \ No newline at end of file diff --git a/starter-pack/examples/example-state.json b/starter-pack/examples/example-state.json index efb2372..1bb98b8 100644 --- a/starter-pack/examples/example-state.json +++ b/starter-pack/examples/example-state.json @@ -1,28 +1,64 @@ { "gameDetails": { - "round": 26, + "round": 12, "mapWidth": 8, "mapHeight": 4, + "roundIncomeEnergy": 5, "buildingPrices": { - "ENERGY": 20, "DEFENSE": 30, - "ATTACK": 30 + "ATTACK": 30, + "ENERGY": 20 + }, + "buildingsStats": { + "DEFENSE": { + "health": 20, + "constructionTime": 4, + "price": 30, + "weaponDamage": 0, + "weaponSpeed": 0, + "weaponCooldownPeriod": 0, + "energyGeneratedPerTurn": 0, + "destroyMultiplier": 1, + "constructionScore": 1 + }, + "ATTACK": { + "health": 5, + "constructionTime": 2, + "price": 30, + "weaponDamage": 5, + "weaponSpeed": 1, + "weaponCooldownPeriod": 3, + "energyGeneratedPerTurn": 0, + "destroyMultiplier": 1, + "constructionScore": 1 + }, + "ENERGY": { + "health": 5, + "constructionTime": 2, + "price": 20, + "weaponDamage": 0, + "weaponSpeed": 0, + "weaponCooldownPeriod": 0, + "energyGeneratedPerTurn": 3, + "destroyMultiplier": 1, + "constructionScore": 1 + } } }, "players": [ { "playerType": "A", - "energy": 289, - "health": 100, - "hitsTaken": 0, - "score": 363 + "energy": 35, + "health": 95, + "hitsTaken": 1, + "score": 145 }, { "playerType": "B", - "energy": 289, + "energy": 9, "health": 100, "hitsTaken": 0, - "score": 363 + "score": 578 } ], "gameMap": [ @@ -75,7 +111,24 @@ { "x": 4, "y": 0, - "buildings": [], + "buildings": [ + { + "health": 5, + "constructionTimeLeft": -1, + "price": 20, + "weaponDamage": 0, + "weaponSpeed": 0, + "weaponCooldownTimeLeft": 0, + "weaponCooldownPeriod": 0, + "destroyMultiplier": 1, + "constructionScore": 1, + "energyGeneratedPerTurn": 3, + "buildingType": "ENERGY", + "x": 4, + "y": 0, + "playerType": "B" + } + ], "missiles": [], "cellOwner": "B" }, @@ -96,24 +149,7 @@ { "x": 7, "y": 0, - "buildings": [ - { - "health": 5, - "constructionTimeLeft": -1, - "price": 20, - "weaponDamage": 0, - "weaponSpeed": 0, - "weaponCooldownTimeLeft": 0, - "weaponCooldownPeriod": 0, - "destroyMultiplier": 1, - "constructionScore": 1, - "energyGeneratedPerTurn": 3, - "buildingType": "ENERGY", - "x": 7, - "y": 0, - "playerType": "B" - } - ], + "buildings": [], "missiles": [], "cellOwner": "B" } @@ -188,24 +224,7 @@ { "x": 7, "y": 1, - "buildings": [ - { - "health": 5, - "constructionTimeLeft": -1, - "price": 20, - "weaponDamage": 0, - "weaponSpeed": 0, - "weaponCooldownTimeLeft": 0, - "weaponCooldownPeriod": 0, - "destroyMultiplier": 1, - "constructionScore": 1, - "energyGeneratedPerTurn": 3, - "buildingType": "ENERGY", - "x": 7, - "y": 1, - "playerType": "B" - } - ], + "buildings": [], "missiles": [], "cellOwner": "B" } @@ -214,24 +233,7 @@ { "x": 0, "y": 2, - "buildings": [ - { - "health": 5, - "constructionTimeLeft": -1, - "price": 20, - "weaponDamage": 0, - "weaponSpeed": 0, - "weaponCooldownTimeLeft": 0, - "weaponCooldownPeriod": 0, - "destroyMultiplier": 1, - "constructionScore": 1, - "energyGeneratedPerTurn": 3, - "buildingType": "ENERGY", - "x": 0, - "y": 2, - "playerType": "A" - } - ], + "buildings": [], "missiles": [], "cellOwner": "A" }, @@ -252,8 +254,33 @@ { "x": 3, "y": 2, - "buildings": [], - "missiles": [], + "buildings": [ + { + "health": 20, + "constructionTimeLeft": -1, + "price": 30, + "weaponDamage": 0, + "weaponSpeed": 0, + "weaponCooldownTimeLeft": 0, + "weaponCooldownPeriod": 0, + "destroyMultiplier": 1, + "constructionScore": 1, + "energyGeneratedPerTurn": 0, + "buildingType": "DEFENSE", + "x": 3, + "y": 2, + "playerType": "A" + } + ], + "missiles": [ + { + "damage": 5, + "speed": 1, + "x": 3, + "y": 2, + "playerType": "A" + } + ], "cellOwner": "A" }, { @@ -273,8 +300,33 @@ { "x": 6, "y": 2, - "buildings": [], - "missiles": [], + "buildings": [ + { + "health": 20, + "constructionTimeLeft": 2, + "price": 30, + "weaponDamage": 0, + "weaponSpeed": 0, + "weaponCooldownTimeLeft": 0, + "weaponCooldownPeriod": 0, + "destroyMultiplier": 1, + "constructionScore": 1, + "energyGeneratedPerTurn": 0, + "buildingType": "DEFENSE", + "x": 6, + "y": 2, + "playerType": "B" + } + ], + "missiles": [ + { + "damage": 5, + "speed": 1, + "x": 6, + "y": 2, + "playerType": "B" + } + ], "cellOwner": "B" }, { @@ -284,15 +336,15 @@ { "health": 5, "constructionTimeLeft": -1, - "price": 20, - "weaponDamage": 0, - "weaponSpeed": 0, - "weaponCooldownTimeLeft": 0, - "weaponCooldownPeriod": 0, + "price": 30, + "weaponDamage": 5, + "weaponSpeed": 1, + "weaponCooldownTimeLeft": 3, + "weaponCooldownPeriod": 3, "destroyMultiplier": 1, "constructionScore": 1, - "energyGeneratedPerTurn": 3, - "buildingType": "ENERGY", + "energyGeneratedPerTurn": 0, + "buildingType": "ATTACK", "x": 7, "y": 2, "playerType": "B" @@ -372,27 +424,10 @@ { "x": 7, "y": 3, - "buildings": [ - { - "health": 5, - "constructionTimeLeft": -1, - "price": 20, - "weaponDamage": 0, - "weaponSpeed": 0, - "weaponCooldownTimeLeft": 0, - "weaponCooldownPeriod": 0, - "destroyMultiplier": 1, - "constructionScore": 1, - "energyGeneratedPerTurn": 3, - "buildingType": "ENERGY", - "x": 7, - "y": 3, - "playerType": "B" - } - ], + "buildings": [], "missiles": [], "cellOwner": "B" } ] ] -} +} \ No newline at end of file diff --git a/starter-pack/game-config.properties b/starter-pack/game-config.properties new file mode 100644 index 0000000..bdce9de --- /dev/null +++ b/starter-pack/game-config.properties @@ -0,0 +1,45 @@ +#Game Config +game.config.map-width = 8 +game.config.map-height = 4 +game.config.max-rounds = 400 +game.config.start-energy = 20 +game.config.round-income-energy = 5 +game.config.starting-health = 100 +game.config.health-score-multiplier = 100 +game.config.energy-score-multiplier = 1 + +#Basic Wall Config +game.config.defense.config.health = 20 +game.config.defense.config.construction-time-left = 3 +game.config.defense.config.price = 30 +game.config.defense.config.weapon-damage = 0 +game.config.defense.config.weapon-speed = 0 +game.config.defense.config.weapon-cooldown-period = 0 +game.config.defense.config.icon = D +game.config.defense.config.destroy-multiplier = 1 +game.config.defense.config.construction-score = 1 +game.config.defense.config.energy-Produced-per-turn = 0 + +#Basic Turret Config +game.config.attack.config.health = 5 +game.config.attack.config.construction-time-left = 1 +game.config.attack.config.price = 30 +game.config.attack.config.weapon-damage = 5 +game.config.attack.config.weapon-speed = 1 +game.config.attack.config.weapon-cooldown-period = 3 +game.config.attack.config.icon = A +game.config.attack.config.destroy-multiplier = 1 +game.config.attack.config.construction-score = 1 +game.config.attack.config.energy-Produced-per-turn = 0 + +#Basic Energy Generator Config +game.config.energy.config.health = 5 +game.config.energy.config.construction-time-left = 1 +game.config.energy.config.price = 20 +game.config.energy.config.weapon-damage = 0 +game.config.energy.config.weapon-speed = 0 +game.config.energy.config.weapon-cooldown-period = 0 +game.config.energy.config.icon = E +game.config.energy.config.destroy-multiplier = 1 +game.config.energy.config.construction-score = 1 +game.config.energy.config.energy-Produced-per-turn = 3 diff --git a/starter-pack/makefile b/starter-pack/makefile index e887211..8930bdb 100644 --- a/starter-pack/makefile +++ b/starter-pack/makefile @@ -1,5 +1,5 @@ default: - java -jar tower-defense-runner-1.0.0.jar + java -jar tower-defence-runner-1.1.1.jar run: - java -jar tower-defense-runner-1.0.0.jar + java -jar tower-defence-runner-1.1.1.jar diff --git a/starter-pack/reference-bot/java/pom.xml b/starter-pack/reference-bot/java/pom.xml index 9b07a0e..3cd38d8 100644 --- a/starter-pack/reference-bot/java/pom.xml +++ b/starter-pack/reference-bot/java/pom.xml @@ -6,7 +6,7 @@ za.co.entelect.challenge reference-bot - 1.0-SNAPSHOT + 1.1-SNAPSHOT diff --git a/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java index 16638fd..61624f6 100644 --- a/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java +++ b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Bot.java @@ -13,31 +13,34 @@ import java.util.stream.Collectors; public class Bot { + private GameState gameState; /** * Constructor + * * @param gameState the game state **/ - public Bot(GameState gameState){ + public Bot(GameState gameState) { this.gameState = gameState; gameState.getGameMap(); } /** * Run + * * @return the result **/ - public String run(){ + public String run() { String command = ""; //If the enemy has an attack building and I don't have a blocking wall, then block from the front. - for (int i = 0; i < gameState.gameDetails.mapHeight; i++){ + for (int i = 0; i < gameState.gameDetails.mapHeight; i++) { int enemyAttackOnRow = getAllBuildingsForPlayer(PlayerType.B, b -> b.buildingType == BuildingType.ATTACK, i).size(); int myDefenseOnRow = getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.DEFENSE, i).size(); - if (enemyAttackOnRow > 0 && myDefenseOnRow == 0){ - if ( canAffordBuilding(BuildingType.DEFENSE)) + if (enemyAttackOnRow > 0 && myDefenseOnRow == 0) { + if (canAffordBuilding(BuildingType.DEFENSE)) command = placeBuildingInRowFromFront(BuildingType.DEFENSE, i); else command = ""; @@ -51,7 +54,7 @@ public String run(){ int enemyAttackOnRow = getAllBuildingsForPlayer(PlayerType.B, b -> b.buildingType == BuildingType.ATTACK, i).size(); int myEnergyOnRow = getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.ENERGY, i).size(); - if (enemyAttackOnRow == 0 && myEnergyOnRow == 0 ) { + if (enemyAttackOnRow == 0 && myEnergyOnRow == 0) { if (canAffordBuilding(BuildingType.ENERGY)) command = placeBuildingInRowFromBack(BuildingType.ENERGY, i); break; @@ -60,21 +63,21 @@ public String run(){ } //If I have a defense building on a row, then build an attack building behind it. - if (command.equals("")){ + if (command.equals("")) { for (int i = 0; i < gameState.gameDetails.mapHeight; i++) { - if ( getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.DEFENSE, i).size() > 0 - && canAffordBuilding(BuildingType.ATTACK)){ + if (getAllBuildingsForPlayer(PlayerType.A, b -> b.buildingType == BuildingType.DEFENSE, i).size() > 0 + && canAffordBuilding(BuildingType.ATTACK)) { command = placeBuildingInRowFromFront(BuildingType.ATTACK, i); } } } //If I don't need to do anything then either attack or defend randomly based on chance (65% attack, 35% defense). - if (command.equals("")){ - if (getEnergy(PlayerType.A) >= getMostExpensiveBuildingPrice()){ - if ((new Random()).nextInt(100) <= 35){ + if (command.equals("")) { + if (getEnergy(PlayerType.A) >= getMostExpensiveBuildingPrice()) { + if ((new Random()).nextInt(100) <= 35) { return placeBuildingRandomlyFromFront(BuildingType.DEFENSE); - }else{ + } else { return placeBuildingRandomlyFromBack(BuildingType.ATTACK); } } @@ -85,13 +88,14 @@ && canAffordBuilding(BuildingType.ATTACK)){ /** * Place building in a random row nearest to the back + * * @param buildingType the building type * @return the result **/ - private String placeBuildingRandomlyFromBack(BuildingType buildingType){ - for (int i = 0; i < gameState.gameDetails.mapWidth/ 2; i ++){ + private String placeBuildingRandomlyFromBack(BuildingType buildingType) { + for (int i = 0; i < gameState.gameDetails.mapWidth / 2; i++) { List listOfFreeCells = getListOfEmptyCellsForColumn(i); - if (!listOfFreeCells.isEmpty()){ + if (!listOfFreeCells.isEmpty()) { CellStateContainer pickedCell = listOfFreeCells.get((new Random()).nextInt(listOfFreeCells.size())); return buildCommand(pickedCell.x, pickedCell.y, buildingType); } @@ -101,13 +105,14 @@ private String placeBuildingRandomlyFromBack(BuildingType buildingType){ /** * Place building in a random row nearest to the front + * * @param buildingType the building type * @return the result **/ - private String placeBuildingRandomlyFromFront(BuildingType buildingType){ - for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--){ + private String placeBuildingRandomlyFromFront(BuildingType buildingType) { + for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--) { List listOfFreeCells = getListOfEmptyCellsForColumn(i); - if (!listOfFreeCells.isEmpty()){ + if (!listOfFreeCells.isEmpty()) { CellStateContainer pickedCell = listOfFreeCells.get((new Random()).nextInt(listOfFreeCells.size())); return buildCommand(pickedCell.x, pickedCell.y, buildingType); } @@ -117,13 +122,14 @@ private String placeBuildingRandomlyFromFront(BuildingType buildingType){ /** * Place building in row y nearest to the front + * * @param buildingType the building type - * @param y the y + * @param y the y * @return the result **/ - private String placeBuildingInRowFromFront(BuildingType buildingType, int y){ - for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--){ - if (isCellEmpty(i, y)){ + private String placeBuildingInRowFromFront(BuildingType buildingType, int y) { + for (int i = (gameState.gameDetails.mapWidth / 2) - 1; i >= 0; i--) { + if (isCellEmpty(i, y)) { return buildCommand(i, y, buildingType); } } @@ -132,13 +138,14 @@ private String placeBuildingInRowFromFront(BuildingType buildingType, int y){ /** * Place building in row y nearest to the back + * * @param buildingType the building type - * @param y the y + * @param y the y * @return the result **/ - private String placeBuildingInRowFromBack(BuildingType buildingType, int y){ - for (int i = 0; i < gameState.gameDetails.mapWidth / 2; i++){ - if (isCellEmpty(i, y)){ + private String placeBuildingInRowFromBack(BuildingType buildingType, int y) { + for (int i = 0; i < gameState.gameDetails.mapWidth / 2; i++) { + if (isCellEmpty(i, y)) { return buildCommand(i, y, buildingType); } } @@ -147,23 +154,25 @@ private String placeBuildingInRowFromBack(BuildingType buildingType, int y){ /** * Construct build command - * @param x the x - * @param y the y + * + * @param x the x + * @param y the y * @param buildingType the building type * @return the result **/ - private String buildCommand(int x, int y, BuildingType buildingType){ + private String buildCommand(int x, int y, BuildingType buildingType) { return String.format("%s,%d,%s", String.valueOf(x), y, buildingType.getCommandCode()); } /** * Get all buildings for player in row y + * * @param playerType the player type - * @param filter the filter - * @param y the y + * @param filter the filter + * @param y the y * @return the result - * **/ - private List getAllBuildingsForPlayer(PlayerType playerType, Predicate filter, int y){ + **/ + private List getAllBuildingsForPlayer(PlayerType playerType, Predicate filter, int y) { return gameState.getGameMap().stream() .filter(c -> c.cellOwner == playerType && c.y == y) .flatMap(c -> c.getBuildings().stream()) @@ -173,10 +182,11 @@ private List getAllBuildingsForPlayer(PlayerType playerType, Predicate /** * Get all empty cells for column x + * * @param x the x * @return the result - * **/ - private List getListOfEmptyCellsForColumn(int x){ + **/ + private List getListOfEmptyCellsForColumn(int x) { return gameState.getGameMap().stream() .filter(c -> c.x == x && isCellEmpty(x, c.y)) .collect(Collectors.toList()); @@ -184,19 +194,20 @@ private List getListOfEmptyCellsForColumn(int x){ /** * Checks if cell at x,y is empty + * * @param x the x * @param y the y * @return the result - * **/ + **/ private boolean isCellEmpty(int x, int y) { Optional cellOptional = gameState.getGameMap().stream() .filter(c -> c.x == x && c.y == y) .findFirst(); - if (cellOptional.isPresent()){ + if (cellOptional.isPresent()) { CellStateContainer cell = cellOptional.get(); return cell.getBuildings().size() <= 0; - }else{ + } else { System.out.println("Invalid cell selected"); } return true; @@ -204,19 +215,21 @@ private boolean isCellEmpty(int x, int y) { /** * Checks if building can be afforded + * * @param buildingType the building type * @return the result - * **/ - private boolean canAffordBuilding(BuildingType buildingType){ + **/ + private boolean canAffordBuilding(BuildingType buildingType) { return getEnergy(PlayerType.A) >= getPriceForBuilding(buildingType); } /** * Gets energy for player type + * * @param playerType the player type * @return the result - * **/ - private int getEnergy(PlayerType playerType){ + **/ + private int getEnergy(PlayerType playerType) { return gameState.getPlayers().stream() .filter(p -> p.playerType == playerType) .mapToInt(p -> p.energy) @@ -225,27 +238,24 @@ private int getEnergy(PlayerType playerType){ /** * Gets price for building type + * * @param buildingType the player type * @return the result - * **/ - private int getPriceForBuilding(BuildingType buildingType){ - return gameState.gameDetails.buildingPrices.get(buildingType); + **/ + private int getPriceForBuilding(BuildingType buildingType) { + return gameState.gameDetails.buildingsStats.get(buildingType).price; } /** * Gets price for most expensive building type + * * @return the result - * **/ - private int getMostExpensiveBuildingPrice(){ - int buildingPrice = 0; - for (Integer value : gameState.gameDetails.buildingPrices.values()){ - if (buildingPrice == 0){ - buildingPrice = value; - } - if (value > buildingPrice){ - buildingPrice = value; - } - } - return buildingPrice; + **/ + private int getMostExpensiveBuildingPrice() { + return gameState.gameDetails.buildingsStats + .values().stream() + .mapToInt(b -> b.price) + .max() + .orElse(0); } } diff --git a/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java index adae0a2..2e04874 100644 --- a/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java +++ b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/Main.java @@ -3,7 +3,7 @@ import com.google.gson.Gson; import za.co.entelect.challenge.entities.GameState; -import java.io.*; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; @@ -13,13 +13,14 @@ public class Main { /** * Read the current state, feed it to the bot, get the output and write it to the command. + * * @param args the args **/ public static void main(String[] args) { String state = null; try { state = new String(Files.readAllBytes(Paths.get(STATE_FILE_NAME))); - }catch (IOException e){ + } catch (IOException e) { e.printStackTrace(); } @@ -34,6 +35,7 @@ public static void main(String[] args) { /** * Write bot response to file + * * @param command the command **/ private static void writeBotResponseToFile(String command) { diff --git a/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java new file mode 100644 index 0000000..298ed11 --- /dev/null +++ b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java @@ -0,0 +1,15 @@ +package za.co.entelect.challenge.entities; + +public class BuildingStats { + + public int health; + public int constructionTime; + public int price; + public int weaponDamage; + public int weaponSpeed; + public int weaponCooldownPeriod; + public int energyGeneratedPerTurn; + public int destroyMultiplier; + public int constructionScore; + +} diff --git a/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java index 187491f..019e6a4 100644 --- a/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java +++ b/starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java @@ -5,9 +5,12 @@ import java.util.HashMap; public class GameDetails { + public int round; public int mapWidth; public int mapHeight; - public HashMap buildingPrices; + public int roundIncomeEnergy; + public HashMap buildingsStats = new HashMap<>(); + } diff --git a/starter-pack/run.bat b/starter-pack/run.bat index ba366e9..787c9bf 100644 --- a/starter-pack/run.bat +++ b/starter-pack/run.bat @@ -1,2 +1,2 @@ -java -jar tower-defence-runner-1.0.1.jar +java -jar tower-defence-runner-1.1.1.jar pause \ No newline at end of file diff --git a/starter-pack/starter-bots/cplusplus/samplebot.exe b/starter-pack/starter-bots/cplusplus/samplebot.exe new file mode 100644 index 0000000..70522e3 Binary files /dev/null and b/starter-pack/starter-bots/cplusplus/samplebot.exe differ diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs index 883411b..deb7073 100644 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs @@ -9,9 +9,11 @@ namespace StarterBot public class Bot { private readonly GameState _gameState; - private readonly int _attackCost; - private readonly int _defenseCost; - private readonly int _energyCost; + + private readonly BuildingStats _attackStats; + private readonly BuildingStats _defenseStats; + private readonly BuildingStats _energyStats; + private readonly int _mapWidth; private readonly int _mapHeight; private readonly Player _player; @@ -22,9 +24,11 @@ public Bot(GameState gameState) this._gameState = gameState; this._mapHeight = gameState.GameDetails.MapHeight; this._mapWidth = gameState.GameDetails.MapWidth; - this._attackCost = gameState.GameDetails.BuildingPrices[BuildingType.Attack]; - this._defenseCost = gameState.GameDetails.BuildingPrices[BuildingType.Defense]; - this._energyCost = gameState.GameDetails.BuildingPrices[BuildingType.Energy]; + + this._attackStats = gameState.GameDetails.BuildingsStats[BuildingType.Attack]; + this._defenseStats = gameState.GameDetails.BuildingsStats[BuildingType.Defense]; + this._energyStats = gameState.GameDetails.BuildingsStats[BuildingType.Energy]; + this._random = new Random((int) DateTime.Now.Ticks); _player = gameState.Players.Single(x => x.PlayerType == PlayerType.A); @@ -35,7 +39,7 @@ public string Run() var commandToReturn = ""; //This will check if there is enough energy to build any building before processing any commands - if (_player.Energy < _defenseCost && _player.Energy < _energyCost && _player.Energy < _attackCost) + if (_player.Energy < _defenseStats.Price || _player.Energy < _energyStats.Price || _player.Energy < _attackStats.Price) { return commandToReturn; } diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs new file mode 100644 index 0000000..62dedb4 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace StarterBot.Entities +{ + public class BuildingStats + { + public int Health; + public int ConstructionTime; + public int Price; + + //Weapon details, applicable only to attack buildings + public int WeaponDamage; + public int WeaponSpeed; + public int WeaponCooldownPeriod; + + // Energy generation details, only applicable to energy buildings + public int EnergyGeneratedPerTurn; + + // Score details + public int DestroyMultiplier; + public int ConstructionScore; + } +} diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs index 447374b..b8c9169 100644 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using StarterBot.Enums; namespace StarterBot.Entities @@ -6,8 +7,8 @@ namespace StarterBot.Entities public class GameDetails { public int Round { get; set; } - public int MapWidth { get; set; } - public int MapHeight { get; set; } - public Dictionary BuildingPrices { get; set; } + public int MapWidth { get; set; } + public int MapHeight { get; set; } + public Dictionary BuildingsStats { get; set; } } } \ No newline at end of file diff --git a/starter-pack/starter-bots/golang/README.md b/starter-pack/starter-bots/golang/README.md new file mode 100644 index 0000000..d7158f4 --- /dev/null +++ b/starter-pack/starter-bots/golang/README.md @@ -0,0 +1,17 @@ +# Go Sample Bot + +A naive and hacky version of a bot in Go. + +## Go runtime + +Find the relevant Go installation files here: https://golang.org/dl/. + +To find out more about the Go language, visit the [project website](https://golang.org). + +## Running + +The game runner will combine compile and execute using the `run` command, rather than as separate steps. For example: + +``` +go run golangbot.go +``` diff --git a/starter-pack/starter-bots/golang/bot.json b/starter-pack/starter-bots/golang/bot.json new file mode 100644 index 0000000..87143f2 --- /dev/null +++ b/starter-pack/starter-bots/golang/bot.json @@ -0,0 +1,8 @@ +{ + "author":"John Doe", + "email":"john.doe@example.com", + "nickName" :"Bob", + "botLocation": "/", + "botFileName": "starterbot.go", + "botLanguage": "golang" +} \ No newline at end of file diff --git a/starter-pack/starter-bots/golang/starterbot.go b/starter-pack/starter-bots/golang/starterbot.go new file mode 100644 index 0000000..11f3f73 --- /dev/null +++ b/starter-pack/starter-bots/golang/starterbot.go @@ -0,0 +1,233 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "time" +) + +const ( + Defense string = "DEFENSE" + Attack string = "ATTACK" + Energy string = "ENERGY" +) + +type Coord struct { + X int + Y int +} + +type BuildingPrices struct { + Defense int `json:"DEFENSE"` + Attack int `json:"ATTACK"` + Energy int `json:"ENERGY"` +} + +var buildingPrice = map[string]int{ + "DEFENSE": 0, + "ATTACK": 0, + "ENERGY": 0, +} + +var buildingCommandVal = map[string]int{ + "DEFENSE": 0, + "ATTACK": 1, + "ENERGY": 2, +} + +type GameDetails struct { + Round int `json:"round"` + MapWidth int `json:"mapWidth"` + MapHeight int `json:"mapHeight"` + BuildingPrices `json:"buildingPrices"` +} + +type Player struct { + PlayerType string `json:"playerType"` + Energy int `json:"energy"` + Health int `json:"health"` +} + +type Building struct { + X int `json:"x"` + Y int `json:"y"` + Health int `json:"health"` + PlayerType string `json:"playerType"` +} + +type Missile struct { + X int `json:"x"` + Y int `json:"y"` + PlayerType string `json:"playerType"` +} + +type Cell struct { + X int `json:"x"` + Y int `json:"y"` + Buildings []Building `json:"buildings"` + Missiles []Missile `json:"missiles"` + CellOwner string `json:"cellOwner"` +} + +type GameState struct { + GameDetails `json:"gameDetails"` + Players []Player `json:"players"` + GameMap [][]Cell `json:"gameMap"` +} + +const stateFilename = "state.json" +const commandFilename = "command.txt" + +var command string +var gameState GameState +var gameDetails GameDetails +var myself Player +var opponent Player +var gameMap [][]Cell +var missiles []Missile +var buildings []Building + +func main() { + runGameCycle() + writeCommand() +} + +func writeCommand() { + err := ioutil.WriteFile(commandFilename, []byte(command), 0666) + if err != nil { + panic(err) + } +} + +func init() { + rand.Seed(time.Now().Unix()) + + data, err := ioutil.ReadFile(stateFilename) + if err != nil { + panic(err.Error()) + } + + var gameState GameState + err = json.Unmarshal(data, &gameState) + if err != nil { + panic(err.Error()) + } + + // load some convenience variables + gameDetails = gameState.GameDetails + gameMap = gameState.GameMap + buildingPrice[Attack] = gameDetails.BuildingPrices.Attack + buildingPrice[Defense] = gameDetails.BuildingPrices.Defense + buildingPrice[Energy] = gameDetails.BuildingPrices.Energy + + for _, player := range gameState.Players { + switch player.PlayerType { + case "A": + myself = player + case "B": + opponent = player + } + } + + for x := 0; x < gameDetails.MapHeight; x++ { + for y := 0; y < gameDetails.MapWidth; y++ { + cell := gameMap[x][y] + for missileIndex := 0; missileIndex < len(cell.Missiles); missileIndex++ { + missiles = append(missiles, cell.Missiles[missileIndex]) + } + for buildingIndex := 0; buildingIndex < len(cell.Buildings); buildingIndex++ { + buildings = append(buildings, cell.Buildings[buildingIndex]) + } + } + } +} + +func runGameCycle() { + var row int + var coord = Coord{-1, -1} + + if underAttack(&row) && canBuild(Defense) { + coord = chooseLocationToDefend(row) + buildBuilding(Defense, coord) + } else if canBuild(Attack) { + buildBuilding(Attack, coord) + } else { + doNothing() + } +} + +func underAttack(row *int) bool { + *row = -1 + for _, missile := range missiles { + if missile.PlayerType == opponent.PlayerType { + *row = missile.Y + break + } + } + return *row >= 0 +} + +func chooseLocationToDefend(row int) Coord { + var col = 0 + for _, building := range buildings { + if building.PlayerType == myself.PlayerType && building.Y == row { + if building.X > col { + col = building.X + } + } + } + if col >= (gameDetails.MapWidth/2)-1 { + return randomUnoccupiedCoordinate() + } + + return Coord{X: col + 1, Y: row} +} + +func canBuild(buildingType string) bool { + return myself.Energy >= buildingPrice[buildingType] +} + +func buildBuilding(buildingType string, coord Coord) { + if coord.X < 0 || coord.Y < 0 { + coord = randomUnoccupiedCoordinate() + } + command = fmt.Sprintf("%d,%d,%d", coord.X, coord.Y, buildingCommandVal[buildingType]) +} + +func doNothing() { + command = "" +} + +func randomCoordinate() Coord { + var coord = Coord{} + coord.X = rand.Intn(gameDetails.MapWidth / 2) + coord.Y = rand.Intn(gameDetails.MapHeight) + return coord +} + +func randomUnoccupiedCoordinate() Coord { + var coord Coord + + for { + coord = randomCoordinate() + if isOccupied(coord) == false { + break + } + } + return coord +} + +func isOccupied(coord Coord) bool { + if coord.X < 0 || coord.X >= gameDetails.MapWidth || coord.Y < 0 || coord.Y >= gameDetails.MapHeight { + return false + } + var cell = gameMap[coord.X][coord.Y] + return len(cell.Buildings) != 0 +} + +func prettyPrint(v interface{}) { + b, _ := json.MarshalIndent(v, "", " ") + println(string(b)) +} diff --git a/starter-pack/starter-bots/haskell/.gitignore b/starter-pack/starter-bots/haskell/.gitignore new file mode 100644 index 0000000..ad04ed9 --- /dev/null +++ b/starter-pack/starter-bots/haskell/.gitignore @@ -0,0 +1,15 @@ +# Editor files +*~ +TAGS + +# Project (stack) files +.stack-work/ +EntelectChallenge2018.cabal + +# Compiled files +*.o +bin/ + +# Game files +command.txt +state.json \ No newline at end of file diff --git a/starter-pack/starter-bots/haskell/ChangeLog.md b/starter-pack/starter-bots/haskell/ChangeLog.md new file mode 100644 index 0000000..0ac05d8 --- /dev/null +++ b/starter-pack/starter-bots/haskell/ChangeLog.md @@ -0,0 +1,3 @@ +# Changelog for EntelectChallenge2018 + +## Unreleased changes diff --git a/starter-pack/starter-bots/haskell/LICENSE b/starter-pack/starter-bots/haskell/LICENSE new file mode 100644 index 0000000..67fcee8 --- /dev/null +++ b/starter-pack/starter-bots/haskell/LICENSE @@ -0,0 +1,14 @@ +Copyright Edward John Steere (c) 2018 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/starter-pack/starter-bots/haskell/README.md b/starter-pack/starter-bots/haskell/README.md new file mode 100644 index 0000000..d50ec75 --- /dev/null +++ b/starter-pack/starter-bots/haskell/README.md @@ -0,0 +1,26 @@ +# Haskell Sample Bot +Haskell is a purely functional programming language. You can find out +more about Haskell [here](https://www.haskell.org/). + +## Environment Requirements +Install the [Haskell Platform](https://www.haskell.org/platform/) and +ensure that the `stack` executable is on the path. + +## Building +Simply run: + +``` +stack install --local-bin-path bin +``` + +to build the binary and put it into a folder in the root of the +project called `bin`. + +## Running +Haskell creates native binaries so you can simply run: + +``` +./bin/EntelectChallenge2018-exe +``` + +from the command line to invoke the bot program. diff --git a/starter-pack/starter-bots/haskell/Setup.hs b/starter-pack/starter-bots/haskell/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/starter-pack/starter-bots/haskell/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/starter-pack/starter-bots/haskell/app/Main.hs b/starter-pack/starter-bots/haskell/app/Main.hs new file mode 100644 index 0000000..46b4d15 --- /dev/null +++ b/starter-pack/starter-bots/haskell/app/Main.hs @@ -0,0 +1,10 @@ +module Main where + +import Interpretor (repl) +import Bot (decide) +import System.Random + +main :: IO () +main = do + gen <- getStdGen + repl (decide gen) diff --git a/starter-pack/starter-bots/haskell/bot.json b/starter-pack/starter-bots/haskell/bot.json new file mode 100644 index 0000000..ed3c743 --- /dev/null +++ b/starter-pack/starter-bots/haskell/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "Bill", + "botLocation": "/bin", + "botFileName": "EntelectChallenge2018-exe", + "botLanguage": "haskell" +} diff --git a/starter-pack/starter-bots/haskell/package.yaml b/starter-pack/starter-bots/haskell/package.yaml new file mode 100644 index 0000000..f1e0960 --- /dev/null +++ b/starter-pack/starter-bots/haskell/package.yaml @@ -0,0 +1,54 @@ +name: EntelectChallenge2018 +version: 0.1.0.0 +github: "quiescent/EntelectChallenge2018" +license: GPL-3 +author: "Edward John Steere" +maintainer: "edward.steere@gmail.com" +copyright: "2018 Edward John Steere" + +extra-source-files: +- README.md +- ChangeLog.md + +# Metadata used when publishing your package +# synopsis: Short description of your package +# category: Web + +# To avoid duplicated efforts in documentation and dealing with the +# complications of embedding Haddock markup inside cabal files, it is +# common to point users to the README.md file. +description: Please see the README on GitHub at + +dependencies: +- base >= 4.7 && < 5 +- aeson >= 1.2.4.0 +- containers >= 0.5.10.0 +- vector >= 0.12.0.1 +- random >= 1.1 +- bytestring >= 0.10.8.2 + +library: + source-dirs: src + +executables: + EntelectChallenge2018-exe: + main: Main.hs + source-dirs: app + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - EntelectChallenge2018 + +tests: + EntelectChallenge2018-test: + main: Spec.hs + source-dirs: test + buildable: false + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - EntelectChallenge2018 diff --git a/starter-pack/starter-bots/haskell/src/Bot.hs b/starter-pack/starter-bots/haskell/src/Bot.hs new file mode 100644 index 0000000..550db49 --- /dev/null +++ b/starter-pack/starter-bots/haskell/src/Bot.hs @@ -0,0 +1,122 @@ +module Bot + where + +import Interpretor (GameState(..), + Command, + GameDetails(..), + Building(..), + CellStateContainer(..), + PlayerType(..), + BuildingType(..), + BuildingPriceIndex(..), + Player(..)) +import Data.List +import System.Random +import Control.Monad + +-- Predicate combination operator +(&&&) :: (a -> Bool) -> (a -> Bool) -> (a -> Bool) +(&&&) f g = \ input -> f input && g input + +cellBelongsTo :: PlayerType -> CellStateContainer -> Bool +cellBelongsTo typeOfPlayer = + (==typeOfPlayer) . cellOwner + +cellContainsBuildingType :: BuildingType -> CellStateContainer -> Bool +cellContainsBuildingType typeOfBuilding = + any ((==typeOfBuilding) . buildingType) . buildings + +enemyHasAttacking :: GameState -> Int -> Bool +enemyHasAttacking state = + any cellContainsEnemyAttacker . ((gameMap state) !!) + where + cellContainsEnemyAttacker = + (cellBelongsTo B) &&& (cellContainsBuildingType ATTACK) + +cellBelongsToMe :: CellStateContainer -> Bool +cellBelongsToMe = cellBelongsTo A + +iDontHaveDefense :: GameState -> Int -> Bool +iDontHaveDefense state = + not . any cellContainDefenseFromMe . ((gameMap state) !!) + where + cellContainDefenseFromMe = + cellBelongsToMe &&& (cellContainsBuildingType DEFENSE) + +thereIsAnEmptyCellInRow :: GameState -> Int -> Bool +thereIsAnEmptyCellInRow (GameState {gameMap = gameMap'})= + any cellIsEmpty . (gameMap' !!) + +indexOfFirstEmpty :: GameState -> Int -> Maybe Int +indexOfFirstEmpty (GameState {gameMap = gameMap'}) = + fmap yPos . find (cellIsEmpty &&& cellBelongsToMe) . (gameMap' !!) + +defendAttack :: GameState -> Maybe (Int, Int, BuildingType) +defendAttack state@(GameState _ _ (GameDetails _ _ height _)) = do + x <- find rowUnderAttack [0..height - 1] + y <- indexOfFirstEmpty state x + return (x, y, DEFENSE) + where + rowUnderAttack = (enemyHasAttacking state) &&& + (iDontHaveDefense state) &&& + (thereIsAnEmptyCellInRow state) + +hasEnoughEnergyForMostExpensiveBuilding :: GameState -> Bool +hasEnoughEnergyForMostExpensiveBuilding state@(GameState _ _ (GameDetails { buildingPrices = prices })) = + ourEnergy >= maxPrice + where + ourEnergy = energy ourPlayer + ourPlayer = (head . filter ((==A) . playerType) . players) state + maxPrice = maximum towerPrices + towerPrices = map ($ prices) [attackTowerCost, defenseTowerCost, energyTowerCost] + +cellIsEmpty :: CellStateContainer -> Bool +cellIsEmpty = ([] ==) . buildings + +myEmptyCells :: [[CellStateContainer]] -> [CellStateContainer] +myEmptyCells = + concat . map (filter isMineAndIsEmpty) + where + isMineAndIsEmpty = cellIsEmpty &&& cellBelongsToMe + +randomEmptyCell :: RandomGen g => g -> GameState -> ((Int, Int), g) +randomEmptyCell gen (GameState {gameMap = mapGrid}) = + let emptyCells = myEmptyCells mapGrid + (randomInt, newGenerator) = next gen + emptyCell = emptyCells !! mod randomInt (length emptyCells) + in ((xPos emptyCell, yPos emptyCell), newGenerator) + +randomBuilding :: RandomGen g => g -> (BuildingType, g) +randomBuilding gen = + let (randomInt, gen') = next gen + buildingIndex = mod randomInt 3 + in (case buildingIndex of + 0 -> DEFENSE + 1 -> ATTACK + _ -> ENERGY, + gen') + +buildRandomly :: RandomGen g => g -> GameState -> Maybe (Int, Int, BuildingType) +buildRandomly gen state = + if not $ hasEnoughEnergyForMostExpensiveBuilding state + then Nothing + else let ((x, y), gen') = randomEmptyCell gen state + (building, _) = randomBuilding gen' + in Just (x, y, building) + +doNothingCommand :: Command +doNothingCommand = "" + +build :: Int -> Int -> BuildingType -> Command +build x y buildingType' = + show x ++ "," ++ show y ++ "," ++ + case buildingType' of + DEFENSE -> "0" + ATTACK -> "1" + ENERGY -> "2" + +decide :: RandomGen g => g -> GameState -> Command +decide gen state = + case msum [defendAttack state, buildRandomly gen state] of + Just (x, y, building) -> build x y building + Nothing -> doNothingCommand diff --git a/starter-pack/starter-bots/haskell/src/Interpretor.hs b/starter-pack/starter-bots/haskell/src/Interpretor.hs new file mode 100644 index 0000000..09b410f --- /dev/null +++ b/starter-pack/starter-bots/haskell/src/Interpretor.hs @@ -0,0 +1,223 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE FlexibleInstances #-} + +module Interpretor (repl, + Player(..), + PlayerType(..), + Missile(..), + Cell(..), + BuildingType(..), + Building(..), + CellStateContainer(..), + BuildingPriceIndex(..), + GameDetails(..), + GameState(..), + Command) + where + +import Data.Aeson (decode, + FromJSON, + parseJSON, + withObject, + (.:), + ToJSON, + toJSON, + object, + (.=)) +import Data.Vector as V +import GHC.Generics (Generic) +import Data.ByteString.Lazy as B + +data PlayerType = + A | B deriving (Show, Generic, Eq) + +instance FromJSON PlayerType +instance ToJSON PlayerType + +data Player = Player { playerType :: PlayerType, + energy :: Int, + health :: Int, + hitsTaken :: Int, + score :: Int } + deriving (Show, Generic, Eq) + +instance FromJSON Player +instance ToJSON Player + +data Missile = Missile { damage :: Int, speed :: Int } + deriving (Show, Generic, Eq) + +instance FromJSON Missile +instance ToJSON Missile + +data Cell = Cell { x :: Int, y :: Int, owner :: PlayerType } + deriving (Show, Generic, Eq) + +instance FromJSON Cell +instance ToJSON Cell + +data BuildingType = DEFENSE | ATTACK | ENERGY + deriving (Show, Generic, Eq) + +instance FromJSON BuildingType +instance ToJSON BuildingType + +data Building = Building { integrity :: Int, + constructionTimeLeft :: Int, + price :: Int, + weaponDamage :: Int, + weaponSpeed :: Int, + weaponCooldownTimeLeft :: Int, + weaponCooldownPeriod :: Int, + destroyMultiplier :: Int, + constructionScore :: Int, + energyGeneratedPerTurn :: Int, + buildingType :: BuildingType, + buildingX :: Int, + buildingY :: Int, + buildingOwner :: PlayerType } + deriving (Show, Generic, Eq) + +instance FromJSON Building where + parseJSON = withObject "Building" $ \ v -> + Building <$> v .: "health" + <*> v .: "constructionTimeLeft" + <*> v .: "price" + <*> v .: "weaponDamage" + <*> v .: "weaponSpeed" + <*> v .: "weaponCooldownTimeLeft" + <*> v .: "weaponCooldownPeriod" + <*> v .: "destroyMultiplier" + <*> v .: "constructionScore" + <*> v .: "energyGeneratedPerTurn" + <*> v .: "buildingType" + <*> v .: "x" + <*> v .: "y" + <*> v .: "playerType" +instance ToJSON Building where + toJSON (Building integrity' + constructionTimeLeft' + price' + weaponDamage' + weaponSpeed' + weaponCooldownTimeLeft' + weaponCooldownPeriod' + destroyMultiplier' + constructionScore' + energyGeneratedPerTurn' + buildingType' + buildingX' + buildingY' + buildingOwner') = + object ["health" .= integrity', + "constructionTimeLeft" .= constructionTimeLeft', + "price" .= price', + "weaponDamage" .= weaponDamage', + "weaponSpeed" .= weaponSpeed', + "weaponCooldownTimeLeft" .= weaponCooldownTimeLeft', + "weaponCooldownPeriod" .= weaponCooldownPeriod', + "destroyMultiplier" .= destroyMultiplier', + "constructionScore" .= constructionScore', + "energyGeneratedPerTurn" .= energyGeneratedPerTurn', + "buildingType" .= buildingType', + "x" .= buildingX', + "y" .= buildingY', + "playerType" .= buildingOwner'] + +data CellStateContainer = CellStateContainer { xPos :: Int, + yPos :: Int, + cellOwner :: PlayerType, + buildings :: [Building], + missiles :: [Missile] } + deriving (Show, Generic, Eq) + +instance FromJSON CellStateContainer where + parseJSON = withObject "CellStateContainer" $ \ v -> do + x' <- v .: "x" + y' <- v .: "y" + cellOwner' <- v .: "cellOwner" + buildings' <- v .: "buildings" + buildings'' <- Prelude.mapM parseJSON $ V.toList buildings' + missiles' <- v .: "missiles" + missiles'' <- Prelude.mapM parseJSON $ V.toList missiles' + return $ CellStateContainer x' + y' + cellOwner' + buildings'' + missiles'' + +instance ToJSON CellStateContainer where + toJSON (CellStateContainer xPos' + yPos' + cellOwner' + buildings' + missiles') = + object ["x" .= xPos', + "y" .= yPos', + "cellOwner" .= cellOwner', + "buildings" .= buildings', + "missiles" .= missiles'] + +data BuildingPriceIndex = BuildingPriceIndex { attackTowerCost :: Int, + defenseTowerCost :: Int, + energyTowerCost :: Int } + deriving (Show, Generic, Eq) + +instance FromJSON BuildingPriceIndex where + parseJSON = withObject "BuildingPriceIndex" $ \ v -> + BuildingPriceIndex <$> v .: "ATTACK" + <*> v .: "DEFENSE" + <*> v .: "ENERGY" +instance ToJSON BuildingPriceIndex where + toJSON (BuildingPriceIndex attackCost defenseCost energyCost) = + object ["ATTACK" .= attackCost, + "DEFENSE" .= defenseCost, + "ENERGY" .= energyCost] + +data GameDetails = GameDetails { round :: Int, + mapWidth :: Int, + mapHeight :: Int, + buildingPrices :: BuildingPriceIndex } + deriving (Show, Generic, Eq) + +instance FromJSON GameDetails +instance ToJSON GameDetails + +data GameState = GameState { players :: [Player], + gameMap :: [[CellStateContainer]], + gameDetails :: GameDetails } + deriving (Show, Generic, Eq) + +instance FromJSON GameState where + parseJSON = withObject "GameState" $ \ v -> do + playersProp <- v .: "players" + playersList <- Prelude.mapM parseJSON $ V.toList playersProp + gameMapObject <- v .: "gameMap" + gameMapProp <- Prelude.mapM parseJSON $ V.toList gameMapObject + gameDetailsProp <- v .: "gameDetails" + return $ GameState playersList gameMapProp gameDetailsProp + +instance ToJSON GameState where + toJSON (GameState gamePlayers mapForGame details) = + object ["players" .= gamePlayers, "gameMap" .= mapForGame, "gameDetails" .= details] + +stateFilePath :: String +stateFilePath = "state.json" + +commandFilePath :: String +commandFilePath = "command.txt" + +readGameState :: IO GameState +readGameState = do + stateString <- B.readFile stateFilePath + let Just state = decode stateString + return state + +printGameState :: String -> IO () +printGameState command = Prelude.writeFile commandFilePath command + +type Command = String + +repl :: (GameState -> Command) -> IO () +repl evaluate = fmap evaluate readGameState >>= printGameState diff --git a/starter-pack/starter-bots/haskell/stack.yaml b/starter-pack/starter-bots/haskell/stack.yaml new file mode 100644 index 0000000..eb506f9 --- /dev/null +++ b/starter-pack/starter-bots/haskell/stack.yaml @@ -0,0 +1,66 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# resolver: ghcjs-0.1.0_ghc-7.10.2 +# resolver: +# name: custom-snapshot +# location: "./custom-snapshot.yaml" +resolver: lts-11.7 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# extra-dep: true +# subdirs: +# - auto-update +# - wai +# +# A package marked 'extra-dep: true' will only be built if demanded by a +# non-dependency (i.e. a user package), and its test suites and benchmarks +# will not be run. This is useful for tweaking upstream packages. +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# (e.g., acme-missiles-0.3) +# extra-deps: [] + +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.6" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor \ No newline at end of file diff --git a/starter-pack/starter-bots/haskell/test/Spec.hs b/starter-pack/starter-bots/haskell/test/Spec.hs new file mode 100644 index 0000000..cd4753f --- /dev/null +++ b/starter-pack/starter-bots/haskell/test/Spec.hs @@ -0,0 +1,2 @@ +main :: IO () +main = putStrLn "Test suite not yet implemented" diff --git a/starter-pack/starter-bots/java/pom.xml b/starter-pack/starter-bots/java/pom.xml index eb5be9e..915ef79 100644 --- a/starter-pack/starter-bots/java/pom.xml +++ b/starter-pack/starter-bots/java/pom.xml @@ -6,7 +6,7 @@ za.co.entelect.challenge java-sample-bot - 1.0-SNAPSHOT + 1.1-SNAPSHOT diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java index 772b711..465afd6 100644 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java @@ -91,9 +91,10 @@ private String buildRandom() { * @return the result **/ private boolean hasEnoughEnergyForMostExpensiveBuilding() { - return gameDetails.buildingPrices.values().stream() - .filter(bp -> bp < myself.energy) - .toArray().length == 3; + return gameDetails.buildingsStats.values().stream() + .filter(b -> b.price <= myself.energy) + .toArray() + .length == 3; } /** @@ -185,6 +186,6 @@ private boolean getAnyBuildingsForPlayer(PlayerType playerType, Predicate= gameDetails.buildingPrices.get(buildingType); + return myself.energy >= gameDetails.buildingsStats.get(buildingType).price; } } diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java new file mode 100644 index 0000000..298ed11 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java @@ -0,0 +1,15 @@ +package za.co.entelect.challenge.entities; + +public class BuildingStats { + + public int health; + public int constructionTime; + public int price; + public int weaponDamage; + public int weaponSpeed; + public int weaponCooldownPeriod; + public int energyGeneratedPerTurn; + public int destroyMultiplier; + public int constructionScore; + +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java index 565682d..68a56e7 100644 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java @@ -8,6 +8,7 @@ public class GameDetails { public int round; public int mapWidth; public int mapHeight; - public HashMap buildingPrices; + public int roundIncomeEnergy; + public HashMap buildingsStats = new HashMap<>(); } diff --git a/starter-pack/starter-bots/javascript/StarterBot.js b/starter-pack/starter-bots/javascript/StarterBot.js index c142342..d71130e 100644 --- a/starter-pack/starter-bots/javascript/StarterBot.js +++ b/starter-pack/starter-bots/javascript/StarterBot.js @@ -15,7 +15,7 @@ let mapSize = ""; let cells = ""; let buildings = ""; let missiles = ""; -let buildingPrices = []; +let buildingStats = []; // Capture the arguments initBot(process.argv.slice(2)); @@ -34,10 +34,10 @@ function initBot(args) { y: stateFile.gameDetails.mapHeight }; - let prices = stateFile.gameDetails.buildingPrices; - buildingPrices[0]= prices.DEFENSE; - buildingPrices[1]= prices.ATTACK; - buildingPrices[2]= prices.ENERGY; + let stats = stateFile.gameDetails.buildingsStats; + buildingStats[0]= stats.DEFENSE; + buildingStats[1]= stats.ATTACK; + buildingStats[2]= stats.ENERGY; gameMap = stateFile.gameMap; initEntities(); @@ -71,7 +71,7 @@ function isUnderAttack() { let opponentAttackers = buildings.filter(b => b.playerType == 'B' && b.buildingType == 'ATTACK') .filter(b => !myDefenders.some(d => d.y == b.y)); - return (opponentAttackers.length > 0) && (myself.energy >= buildingPrices[0]); + return (opponentAttackers.length > 0) && (myself.energy >= buildingStats[0].price); } function defendRow() { @@ -101,7 +101,7 @@ function defendRow() { } function hasEnoughEnergyForMostExpensiveBuilding() { - return (myself.energy >= Math.max(...buildingPrices)); + return (myself.energy >= Math.max(...(buildingStats.map(stat => stat.price)))); } function buildRandom() { diff --git a/starter-pack/starter-bots/php/README.md b/starter-pack/starter-bots/php/README.md new file mode 100644 index 0000000..7c0ab88 --- /dev/null +++ b/starter-pack/starter-bots/php/README.md @@ -0,0 +1,14 @@ +# PHP Starter Bot + +PHP is a popular general-purpose scripting language that is especially suited to web development. + +Fast, flexible and pragmatic, PHP powers everything from your blog to the most popular websites in the world. + + +## Environment Setup + +The bot requires PHP 7.0 or greater. + +For instructions on installing PHP for your OS, please see the documentation at [http://php.net/manual/en/install.php](http://php.net/manual/en/install.php). + +Also be sure that the PHP CLI binary is in your OS's PATH environment variable. diff --git a/starter-pack/starter-bots/php/StarterBot.php b/starter-pack/starter-bots/php/StarterBot.php new file mode 100644 index 0000000..d4b1cbf --- /dev/null +++ b/starter-pack/starter-bots/php/StarterBot.php @@ -0,0 +1,16 @@ +decideAction()); + +fclose($outputFile); diff --git a/starter-pack/starter-bots/php/bot.json b/starter-pack/starter-bots/php/bot.json new file mode 100644 index 0000000..cfbd92a --- /dev/null +++ b/starter-pack/starter-bots/php/bot.json @@ -0,0 +1,8 @@ +{ + "author":"John Doe", + "email":"john.doe@example.com", + "nickName" :"Engelbert", + "botLocation": "/", + "botFileName": "StarterBot.php", + "botLanguage": "php" +} diff --git a/starter-pack/starter-bots/php/include/Bot.php b/starter-pack/starter-bots/php/include/Bot.php new file mode 100644 index 0000000..f36c9ee --- /dev/null +++ b/starter-pack/starter-bots/php/include/Bot.php @@ -0,0 +1,79 @@ +_game = $state; + $this->_map = $this->_game->getMap(); + } + + /** + * This is the main function for deciding which action to take + * + * Returns a valid action string + */ + public function decideAction() + { + //Check if we should defend + list($x,$y,$building) = $this->checkDefense(); + + //If no defend orders then build randomly + list($x,$y,$building) = $x === null ? $this->buildRandom() : [$x, $y, $building]; + + if ($x !== null && $this->_game->getBuildingPrice($building) <= $this->_game->getPlayerA()->energy) + { + return "$x,$y,$building"; + } + return ""; + } + + /** + * Checks if a row is being attacked and returns a build order if there is an empty space + * and no defensive buildings in the that row. + */ + protected function checkDefense() + { + for ($row = 0; $row < $this->_game->getMapHeight(); $row++) + { + if ($this->_map->isAttackedRow($row) && !$this->_map->rowHasOwnDefense($row)) + { + list($x,$y,$building) = $this->buildDefense($row); + if ($x !== null) + { + return [$x,$y,$building]; + } + } + } + return [null, null, null]; + } + + /** + * Returns defensive build order at last empty cell in a row + */ + protected function buildDefense($row) + { + //Check for last valid empty cell + $x = $this->_map->getLastEmptyCell($row); + return $x === false ? [$x, $y, Map::DEFENSE] : [null, null, null]; + } + + /** + * Returns a random build order on an empty cell + */ + protected function buildRandom() + { + $emptyCells = $this->_map->getValidBuildCells(); + if (!count($emptyCells)) + { + return [null, null, null]; + } + + $cell = $emptyCells[rand(0,count($emptyCells)-1)]; + $building = rand(0,2); + + return [$cell->x,$cell->y,$building]; + } +} diff --git a/starter-pack/starter-bots/php/include/GameState.php b/starter-pack/starter-bots/php/include/GameState.php new file mode 100644 index 0000000..adf36e3 --- /dev/null +++ b/starter-pack/starter-bots/php/include/GameState.php @@ -0,0 +1,98 @@ +_state = json_decode(file_get_contents($filename)); + $_map = null; + } + + /** + * Returns the entire state object for manual processing + */ + public function getState() + { + return $this->_state; + } + + public function getMapWidth() + { + return $this->_state->gameDetails->mapWidth; + } + + public function getMapHeight() + { + return $this->_state->gameDetails->mapHeight; + } + + public function getPlayerA() + { + foreach ($this->_state->players as $player) + { + if ($player->playerType == "A") + { + return $player; + } + } + } + + public function getPlayerB() + { + foreach ($this->_state->players as $player) + { + if ($player->playerType == "B") + { + return $player; + } + } + } + + /** + * Looks up the price of a particular building type + */ + public function getBuildingPrice(int $type) + { + switch ($type) + { + case Map::DEFENSE: + $str = MAP::DEFENSE_STR; + break; + case Map::ATTACK: + $str = MAP::ATTACK_STR; + break; + case Map::ENERGY: + $str = MAP::ENERGY_STR; + break; + default: + return false; + break; + } + return $this->_state->gameDetails->buildingPrices->$str; + } + + /** + * Returns the current round number + */ + public function getRound() + { + return $this->_state->gameDetails->round(); + } + + /** + * Returns a Map object for examining the playing field + */ + public function getMap() + { + if ($this->_map === null) + { + $this->_map = new Map($this->_state->gameMap); + } + + return $this->_map; + } +} diff --git a/starter-pack/starter-bots/php/include/Map.php b/starter-pack/starter-bots/php/include/Map.php new file mode 100644 index 0000000..876cdba --- /dev/null +++ b/starter-pack/starter-bots/php/include/Map.php @@ -0,0 +1,119 @@ +_map = $map; + } + + /** + * Returns the building at a set of coordinates or false if empty + */ + public function getBuilding($x,$y) + { + return count($this->_map[$y][$x]->buildings) ? $this->_map[$y][$x]->buildings[0] : false; + } + + /** + * Returns the missiles at a set of coordinates or false if no missiles + */ + public function getMissiles($x,$y) + { + return count($this->_map[$y][$x]->missiles) ? $this->_map[$y][$x]->missiles : false; + } + + /** + * Returns the x coordinate of the last empty cell in a row + */ + public function getLastEmptyCell($y) + { + for ($x = count($this->_map[$y])/2 - 1; $x >= 0; $x--) + { + if (!$this->getBuilding($x,$y)) + { + return $x; + } + } + return false; + } + + /** + * Returns the x coordinate of the first empty cell in a row + */ + public function getFirstEmptyCell($y) + { + for ($x = 0; $x < count($this->_map[$y])/2; $x++) + { + if (!$this->getBuilding($x,$y)) + { + return $x; + } + } + return false; + } + + /** + * Returns an array of all valid empty build cells + */ + public function getValidBuildCells() + { + $emptyCells = []; + foreach ($this->_map as $row) + { + foreach ($row as $cell) + { + if ($cell->cellOwner == 'A' && !count($cell->buildings)) + { + $emptyCells[] = $cell; + } + } + } + + return $emptyCells; + } + + /** + * Checks if a row is currently under attack by an enemy + */ + public function isAttackedRow($y) + { + foreach ($this->_map[$y] as $cell) + { + foreach ($cell->missiles as $missile) + { + if ($missile->playerType == 'B') + { + return true; + } + } + } + return false; + } + + /** + * Checks if there is a friendly defensive building in a row + */ + public function rowHasOwnDefense($y) + { + foreach ($this->_map[$y] as $cell) + { + foreach ($cell->buildings as $building) + { + if ($building->buildingType == self::DEFENSE_STR && $building->playerType == 'A') + { + return true; + } + } + } + return false; + } +} diff --git a/starter-pack/starter-bots/python3/StarterBot.py b/starter-pack/starter-bots/python3/StarterBot.py index 4b0e81b..110eef8 100644 --- a/starter-pack/starter-bots/python3/StarterBot.py +++ b/starter-pack/starter-bots/python3/StarterBot.py @@ -40,9 +40,33 @@ def __init__(self,state_location): self.round = self.game_state['gameDetails']['round'] - self.prices = {"ATTACK":self.game_state['gameDetails']['buildingPrices']['ATTACK'], - "DEFENSE":self.game_state['gameDetails']['buildingPrices']['DEFENSE'], - "ENERGY":self.game_state['gameDetails']['buildingPrices']['ENERGY']} + self.buildings_stats = {"ATTACK":{"health": self.game_state['gameDetails']['buildingsStats']['ATTACK']['health'], + "constructionTime": self.game_state['gameDetails']['buildingsStats']['ATTACK']['constructionTime'], + "price": self.game_state['gameDetails']['buildingsStats']['ATTACK']['price'], + "weaponDamage": self.game_state['gameDetails']['buildingsStats']['ATTACK']['weaponDamage'], + "weaponSpeed": self.game_state['gameDetails']['buildingsStats']['ATTACK']['weaponSpeed'], + "weaponCooldownPeriod": self.game_state['gameDetails']['buildingsStats']['ATTACK']['weaponCooldownPeriod'], + "energyGeneratedPerTurn": self.game_state['gameDetails']['buildingsStats']['ATTACK']['energyGeneratedPerTurn'], + "destroyMultiplier": self.game_state['gameDetails']['buildingsStats']['ATTACK']['destroyMultiplier'], + "constructionScore": self.game_state['gameDetails']['buildingsStats']['ATTACK']['constructionScore']}, + "DEFENSE":{"health": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['health'], + "constructionTime": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['constructionTime'], + "price": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['price'], + "weaponDamage": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['weaponDamage'], + "weaponSpeed": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['weaponSpeed'], + "weaponCooldownPeriod": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['weaponCooldownPeriod'], + "energyGeneratedPerTurn": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['energyGeneratedPerTurn'], + "destroyMultiplier": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['destroyMultiplier'], + "constructionScore": self.game_state['gameDetails']['buildingsStats']['DEFENSE']['constructionScore']}, + "ENERGY":{"health": self.game_state['gameDetails']['buildingsStats']['ENERGY']['health'], + "constructionTime": self.game_state['gameDetails']['buildingsStats']['ENERGY']['constructionTime'], + "price": self.game_state['gameDetails']['buildingsStats']['ENERGY']['price'], + "weaponDamage": self.game_state['gameDetails']['buildingsStats']['ENERGY']['weaponDamage'], + "weaponSpeed": self.game_state['gameDetails']['buildingsStats']['ENERGY']['weaponSpeed'], + "weaponCooldownPeriod": self.game_state['gameDetails']['buildingsStats']['ENERGY']['weaponCooldownPeriod'], + "energyGeneratedPerTurn": self.game_state['gameDetails']['buildingsStats']['ENERGY']['energyGeneratedPerTurn'], + "destroyMultiplier": self.game_state['gameDetails']['buildingsStats']['ENERGY']['destroyMultiplier'], + "constructionScore": self.game_state['gameDetails']['buildingsStats']['ENERGY']['constructionScore']}} return None @@ -217,7 +241,7 @@ def generateAction(self): if len(self.getUnOccupied(self.player_buildings[i])) == 0: #cannot place anything in a lane with no available cells. continue - elif ( self.checkAttack(i) and (self.player_info['energy'] >= self.prices['DEFENSE']) and (self.checkMyDefense(i)) == False): + elif ( self.checkAttack(i) and (self.player_info['energy'] >= self.buildings_stats['DEFENSE']['price']) and (self.checkMyDefense(i)) == False): #place defense unit if there is an attack building and you can afford a defense building lanes.append(i) #lanes variable will now contain information about all lanes which have attacking units @@ -230,9 +254,9 @@ def generateAction(self): x = random.choice(self.getUnOccupied(self.player_buildings[i])) #otherwise, build a random building type at a random unoccupied location # if you can afford the most expensive building - elif self.player_info['energy'] >= max(s.prices.values()): + elif self.player_info['energy'] >= max(self.buildings_stats['ATTACK']['price'], self.buildings_stats['DEFENSE']['price'], self.buildings_stats['ENERGY']['price']): building = random.choice([0,1,2]) - x = random.randint(0,self.rows) + x = random.randint(0,self.rows-1) y = random.randint(0,int(self.columns/2)-1) else: self.writeDoNothing() diff --git a/starter-pack/tower-defence-runner-1.0.1.jar b/starter-pack/tower-defence-runner-1.1.1.jar similarity index 94% rename from starter-pack/tower-defence-runner-1.0.1.jar rename to starter-pack/tower-defence-runner-1.1.1.jar index 2f2ce67..6dbeb10 100644 Binary files a/starter-pack/tower-defence-runner-1.0.1.jar and b/starter-pack/tower-defence-runner-1.1.1.jar differ