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