From 38eb4e80000fdb41dbcc48562e138f43f799dc7c Mon Sep 17 00:00:00 2001 From: Doug Crawford Date: Tue, 8 May 2018 22:59:30 +0200 Subject: [PATCH 01/20] Initial commit for golang sample bot. --- .../botrunners/BotRunnerFactory.java | 2 + .../challenge/botrunners/GolangBotRunner.java | 17 ++ .../entelect/challenge/enums/BotLanguage.java | 3 + starter-bots/golang/README.md | 17 ++ starter-bots/golang/bot.json | 8 + starter-bots/golang/starterbot.go | 233 ++++++++++++++++++ 6 files changed, 280 insertions(+) create mode 100644 game-runner/src/main/java/za/co/entelect/challenge/botrunners/GolangBotRunner.java create mode 100644 starter-bots/golang/README.md create mode 100644 starter-bots/golang/bot.json create mode 100644 starter-bots/golang/starterbot.go 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..f5f2325 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,8 @@ 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); 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/enums/BotLanguage.java b/game-runner/src/main/java/za/co/entelect/challenge/enums/BotLanguage.java index ba72f63..f5accd0 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 @@ -31,4 +31,7 @@ public enum BotLanguage { @SerializedName("kotlin") KOTLIN, + @SerializedName("golang") + GOLANG, + } 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)) +} From c6ed405b5376dc8450a602b04e954057fa15ccc6 Mon Sep 17 00:00:00 2001 From: PuffyZA Date: Wed, 9 May 2018 15:52:00 +0200 Subject: [PATCH 02/20] Added PHP Starter Bot and Runner --- .../botrunners/BotRunnerFactory.java | 2 + .../challenge/botrunners/PHPBotRunner.java | 18 +++ .../entelect/challenge/enums/BotLanguage.java | 3 + starter-bots/php/StarterBot.php | 16 +++ starter-bots/php/bot.json | 8 ++ starter-bots/php/include/Bot.php | 79 ++++++++++++ starter-bots/php/include/GameState.php | 98 +++++++++++++++ starter-bots/php/include/Map.php | 119 ++++++++++++++++++ 8 files changed, 343 insertions(+) create mode 100644 game-runner/src/main/java/za/co/entelect/challenge/botrunners/PHPBotRunner.java create mode 100644 starter-bots/php/StarterBot.php create mode 100644 starter-bots/php/bot.json create mode 100644 starter-bots/php/include/Bot.php create mode 100644 starter-bots/php/include/GameState.php create mode 100644 starter-bots/php/include/Map.php 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..7dc514d 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,8 @@ public static BotRunner createBotRunner(BotMetaData botMetaData, int timeoutInMi return new Python3BotRunner(botMetaData, timeoutInMilliseconds); case KOTLIN: return new KotlinBotRunner(botMetaData, timeoutInMilliseconds); + case PHP: + return new PHPBotRunner(botMetaData, timeoutInMilliseconds); default: break; } 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/enums/BotLanguage.java b/game-runner/src/main/java/za/co/entelect/challenge/enums/BotLanguage.java index ba72f63..142a722 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,8 @@ public enum BotLanguage { @SerializedName("kotlin") KOTLIN, + + @SerializedName("php") + PHP, } 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..3c74621 --- /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 ($this->_game->getBuildingPrice(Map::DEFENSE) <= $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 ($y = 0; $y < $this->_game->getMapHeight(); $y++) + { + if ($this->_map->isAttackedRow($y) && !$this->_map->rowHasOwnDefense($y)) + { + list($x,$y,$building) = $this->buildDefense($y); + 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($y) + { + //Check for last valid empty cell + $x = $this->_map->getLastEmptyCell($y); + return $x ? [$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; + } +} From 3bf3b14d88c910641538837d3c32bc4052eb78b4 Mon Sep 17 00:00:00 2001 From: PuffyZA Date: Wed, 9 May 2018 17:30:14 +0200 Subject: [PATCH 03/20] Added Readme --- starter-bots/php/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 starter-bots/php/README.md 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. From 789d1e5926549811bd1fcb9522f84b1b9d64f3d0 Mon Sep 17 00:00:00 2001 From: PuffyZA Date: Wed, 9 May 2018 17:30:30 +0200 Subject: [PATCH 04/20] Minor bug fix --- starter-bots/php/include/Bot.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starter-bots/php/include/Bot.php b/starter-bots/php/include/Bot.php index 3c74621..05aade8 100644 --- a/starter-bots/php/include/Bot.php +++ b/starter-bots/php/include/Bot.php @@ -23,7 +23,7 @@ public function decideAction() //If no defend orders then build randomly list($x,$y,$building) = $x === null ? $this->buildRandom() : [$x, $y, $building]; - if ($this->_game->getBuildingPrice(Map::DEFENSE) <= $this->_game->getPlayerA()->energy) + if ($this->_game->getBuildingPrice($building) <= $this->_game->getPlayerA()->energy) { return "$x,$y,$building"; } From 8a238fba5135387772147988dfbdce6e133aa732 Mon Sep 17 00:00:00 2001 From: PuffyZA Date: Wed, 9 May 2018 18:07:39 +0200 Subject: [PATCH 05/20] PHP Starter bot bug fix --- starter-bots/php/include/Bot.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/starter-bots/php/include/Bot.php b/starter-bots/php/include/Bot.php index 05aade8..f36c9ee 100644 --- a/starter-bots/php/include/Bot.php +++ b/starter-bots/php/include/Bot.php @@ -23,7 +23,7 @@ public function decideAction() //If no defend orders then build randomly list($x,$y,$building) = $x === null ? $this->buildRandom() : [$x, $y, $building]; - if ($this->_game->getBuildingPrice($building) <= $this->_game->getPlayerA()->energy) + if ($x !== null && $this->_game->getBuildingPrice($building) <= $this->_game->getPlayerA()->energy) { return "$x,$y,$building"; } @@ -36,11 +36,11 @@ public function decideAction() */ protected function checkDefense() { - for ($y = 0; $y < $this->_game->getMapHeight(); $y++) + for ($row = 0; $row < $this->_game->getMapHeight(); $row++) { - if ($this->_map->isAttackedRow($y) && !$this->_map->rowHasOwnDefense($y)) + if ($this->_map->isAttackedRow($row) && !$this->_map->rowHasOwnDefense($row)) { - list($x,$y,$building) = $this->buildDefense($y); + list($x,$y,$building) = $this->buildDefense($row); if ($x !== null) { return [$x,$y,$building]; @@ -53,11 +53,11 @@ protected function checkDefense() /** * Returns defensive build order at last empty cell in a row */ - protected function buildDefense($y) + protected function buildDefense($row) { //Check for last valid empty cell - $x = $this->_map->getLastEmptyCell($y); - return $x ? [$x, $y, Map::DEFENSE] : [null, null, null]; + $x = $this->_map->getLastEmptyCell($row); + return $x === false ? [$x, $y, Map::DEFENSE] : [null, null, null]; } /** From 24ed545c9f81e225f66e615ff4b672c7bc739903 Mon Sep 17 00:00:00 2001 From: Edward John Steere Date: Wed, 9 May 2018 10:53:58 +0200 Subject: [PATCH 06/20] Add starter bot for Haskell --- starter-bots/haskell/.gitignore | 15 ++ starter-bots/haskell/ChangeLog.md | 3 + starter-bots/haskell/LICENSE | 14 ++ starter-bots/haskell/README.md | 26 +++ starter-bots/haskell/Setup.hs | 2 + starter-bots/haskell/app/Main.hs | 10 ++ starter-bots/haskell/bot.json | 8 + starter-bots/haskell/package.yaml | 54 ++++++ starter-bots/haskell/src/Bot.hs | 122 +++++++++++++ starter-bots/haskell/src/Interpretor.hs | 223 ++++++++++++++++++++++++ starter-bots/haskell/stack.yaml | 66 +++++++ starter-bots/haskell/test/Spec.hs | 2 + 12 files changed, 545 insertions(+) create mode 100644 starter-bots/haskell/.gitignore create mode 100644 starter-bots/haskell/ChangeLog.md create mode 100644 starter-bots/haskell/LICENSE create mode 100644 starter-bots/haskell/README.md create mode 100644 starter-bots/haskell/Setup.hs create mode 100644 starter-bots/haskell/app/Main.hs create mode 100644 starter-bots/haskell/bot.json create mode 100644 starter-bots/haskell/package.yaml create mode 100644 starter-bots/haskell/src/Bot.hs create mode 100644 starter-bots/haskell/src/Interpretor.hs create mode 100644 starter-bots/haskell/stack.yaml create mode 100644 starter-bots/haskell/test/Spec.hs 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..d36c379 --- /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" From 35d03d0f3ec3f097d5fc59c9957748ea7e75182c Mon Sep 17 00:00:00 2001 From: Pierre Roux Date: Wed, 9 May 2018 19:23:57 +0200 Subject: [PATCH 07/20] Add bot-runner for haskell --- .../botrunners/BotRunnerFactory.java | 2 ++ .../botrunners/HaskellBotRunner.java | 25 +++++++++++++++++++ .../entelect/challenge/enums/BotLanguage.java | 3 +++ starter-bots/haskell/bot.json | 4 +-- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 game-runner/src/main/java/za/co/entelect/challenge/botrunners/HaskellBotRunner.java 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 7dc514d..e853fcd 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,8 @@ public static BotRunner createBotRunner(BotMetaData botMetaData, int timeoutInMi return new Python3BotRunner(botMetaData, timeoutInMilliseconds); case KOTLIN: return new KotlinBotRunner(botMetaData, timeoutInMilliseconds); + case HASKELL: + return new HaskellBotRunner(botMetaData, timeoutInMilliseconds); case PHP: return new PHPBotRunner(botMetaData, timeoutInMilliseconds); default: 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/enums/BotLanguage.java b/game-runner/src/main/java/za/co/entelect/challenge/enums/BotLanguage.java index 142a722..28d7df7 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 @@ -34,4 +34,7 @@ public enum BotLanguage { @SerializedName("php") PHP, + @SerializedName("haskell") + HASKELL, + } diff --git a/starter-bots/haskell/bot.json b/starter-bots/haskell/bot.json index d36c379..ed3c743 100644 --- a/starter-bots/haskell/bot.json +++ b/starter-bots/haskell/bot.json @@ -2,7 +2,7 @@ "author": "John Doe", "email": "john.doe@example.com", "nickName": "Bill", - "botLocation": "bin", + "botLocation": "/bin", "botFileName": "EntelectChallenge2018-exe", - "botLanguage": "Haskell" + "botLanguage": "haskell" } From e84cb76ed965ae2e12e96ac8673dfa0d91e83acc Mon Sep 17 00:00:00 2001 From: Gerhard Smit Date: Fri, 11 May 2018 18:40:41 +0200 Subject: [PATCH 08/20] Changed how config is loaded to game-engine --- .../core/engine/TowerDefenseGameEngine.java | 4 ++++ .../co/entelect/challenge/config/GameConfig.java | 14 ++++++++------ game-runner/config.json | 5 +++-- .../co/entelect/challenge/bootstrapper/Config.java | 3 +++ .../challenge/bootstrapper/GameBootstrapper.java | 6 +++--- 5 files changed, 21 insertions(+), 11 deletions(-) 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..5f4ff56 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; 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-runner/config.json b/game-runner/config.json index 814e847..34a06cb 100644 --- a/game-runner/config.json +++ b/game-runner/config.json @@ -1,6 +1,7 @@ { "round-state-output-location": "./tower-defence-matches", "max-runtime-ms": 2000, - "player-a": "../starter-bots/kotlin", - "player-b": "../starter-bots/python3" + "game-config-file-location": "./game-config.properties", + "player-a": "../starter-bots/javascript", + "player-b": "../starter-bots/javascript" } \ No newline at end of file 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..e6ef365 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,7 @@ public class Config { @SerializedName("round-state-output-location") public String roundStateOutputLocation; + @SerializedName("game-config-file-location") + public String gameConfigFileLocation; + } 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..dbda3a8 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 @@ -36,7 +36,7 @@ public static void main(String[] args) { try { Config config = gameBootstrapper.loadConfig(); - gameBootstrapper.prepareEngineRunner(); + gameBootstrapper.prepareEngineRunner(config); gameBootstrapper.prepareHandlers(); gameBootstrapper.prepareGame(config); @@ -60,10 +60,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()); } From da08f107de70214bbc6392fe8b503881959e0c05 Mon Sep 17 00:00:00 2001 From: Pierre Roux Date: Fri, 11 May 2018 18:53:33 +0200 Subject: [PATCH 09/20] Add building blueprints to state.json file Add roundIncomeEnergy to state.json file --- game-engine/core/pom.xml | 2 +- .../challenge/core/entities/GameDetails.java | 21 ++- .../TowerDefenseTextMapRenderer.java | 35 ++--- .../challenge/core/state/RoundState.java | 29 ---- game-engine/domain/pom.xml | 2 +- .../challenge/entities/BuildingStats.java | 43 ++++++ .../challenge/factories/BuildingFactory.java | 6 + game-engine/pom.xml | 2 +- game-runner/config.json | 6 +- game-runner/pom.xml | 2 +- reference-bot/java/pom.xml | 2 +- .../java/za/co/entelect/challenge/Bot.java | 126 ++++++++++-------- .../java/za/co/entelect/challenge/Main.java | 6 +- .../challenge/entities/BuildingStats.java | 15 +++ .../challenge/entities/GameDetails.java | 5 +- starter-bots/cplusplus/samplebot.exe | Bin 0 -> 173751 bytes starter-bots/java/pom.xml | 2 +- .../java/za/co/entelect/challenge/Bot.java | 9 +- .../challenge/entities/BuildingStats.java | 15 +++ .../challenge/entities/GameDetails.java | 3 +- starter-bots/javascript/StarterBot.js | 14 +- 21 files changed, 201 insertions(+), 144 deletions(-) delete mode 100644 game-engine/core/src/main/java/za/co/entelect/challenge/core/state/RoundState.java create mode 100644 game-engine/domain/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java create mode 100644 reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java create mode 100644 starter-bots/cplusplus/samplebot.exe create mode 100644 starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java diff --git a/game-engine/core/pom.xml b/game-engine/core/pom.xml index c9ae450..a3c07d5 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 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/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..59f2d19 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 diff --git a/game-runner/config.json b/game-runner/config.json index 34a06cb..4aa36cd 100644 --- a/game-runner/config.json +++ b/game-runner/config.json @@ -1,7 +1,7 @@ { "round-state-output-location": "./tower-defence-matches", - "max-runtime-ms": 2000, "game-config-file-location": "./game-config.properties", - "player-a": "../starter-bots/javascript", - "player-b": "../starter-bots/javascript" + "max-runtime-ms": 2000, + "player-a": "../starter-bots/java", + "player-b": "../reference-bot/java" } \ No newline at end of file diff --git a/game-runner/pom.xml b/game-runner/pom.xml index 3a5cf76..b92b7fe 100644 --- a/game-runner/pom.xml +++ b/game-runner/pom.xml @@ -6,7 +6,7 @@ za.co.entelect.challenge game-runner - 1.0.0 + 1.1.1 1.8 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 0000000000000000000000000000000000000000..70522e30f68e6957124dd275703e711073c87955 GIT binary patch literal 173751 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�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 literal 0 HcmV?d00001 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() { From 6672ee806cfe43442dab79c8f56cbd588f7e858f Mon Sep 17 00:00:00 2001 From: Pierre Roux Date: Fri, 11 May 2018 19:18:51 +0200 Subject: [PATCH 10/20] Fix game-engine-interface project Add properties file --- game-engine-interface/pom.xml | 2 +- .../game/contracts/game/GameEngine.java | 1 + game-engine/core/pom.xml | 2 +- game-engine/pom.xml | 2 +- game-runner/game-config.properties | 45 +++++++++++++++++++ game-runner/pom.xml | 4 +- 6 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 game-runner/game-config.properties 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..01345ee 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); + void initConfig(String configLocation); } diff --git a/game-engine/core/pom.xml b/game-engine/core/pom.xml index a3c07d5..4ae4252 100644 --- a/game-engine/core/pom.xml +++ b/game-engine/core/pom.xml @@ -27,7 +27,7 @@ za.co.entelect.challenge domain - 1.0.1 + 1.1.1 compile diff --git a/game-engine/pom.xml b/game-engine/pom.xml index 59f2d19..ce6784a 100644 --- a/game-engine/pom.xml +++ b/game-engine/pom.xml @@ -16,7 +16,7 @@ 1.8 - 1.0.0 + 1.0.1 4.12 2.2 2.8.2 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 b92b7fe..bc1c20b 100644 --- a/game-runner/pom.xml +++ b/game-runner/pom.xml @@ -12,8 +12,8 @@ 1.8 0.9.11 2.8.2 - 1.0.0 - 1.0.1 + 1.0.1 + 1.1.1 From b05a35c5aa7922ede271d1362db76624ab14073c Mon Sep 17 00:00:00 2001 From: Pierre Roux Date: Fri, 11 May 2018 20:11:59 +0200 Subject: [PATCH 11/20] Remove initConfig from gameEngine interface --- .../co/entelect/challenge/game/contracts/game/GameEngine.java | 2 +- .../entelect/challenge/core/engine/TowerDefenseGameEngine.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 01345ee..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,5 +5,5 @@ public interface GameEngine { boolean isGameComplete(GameMap gameMap); - void initConfig(String configLocation); + } 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 5f4ff56..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 @@ -20,4 +20,5 @@ public boolean isGameComplete(GameMap gameMap) { return (towerDefenseGameMap.getDeadPlayers().size() > 0); } + } From 0294e3cf657c354ea0e6b849d0475cbb4ef3ff98 Mon Sep 17 00:00:00 2001 From: Daan Keun Date: Fri, 11 May 2018 20:04:06 +0200 Subject: [PATCH 12/20] Make bot use new building stats model rather than prices hashtable --- starter-bots/csharpcore/StarterBot/Bot.cs | 18 +++++++------ .../StarterBot/Entities/BuildingStats.cs | 25 +++++++++++++++++++ .../StarterBot/Entities/GameDetails.cs | 9 ++++--- 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs 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 From 0fe2e313e4c30dfaf994cee8b7fc945e77929f8b Mon Sep 17 00:00:00 2001 From: Daan Keun Date: Fri, 11 May 2018 20:13:13 +0200 Subject: [PATCH 13/20] Run different commands on linux for python bot runners --- .../entelect/challenge/botrunners/Python2BotRunner.java | 9 ++++++++- .../entelect/challenge/botrunners/Python3BotRunner.java | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) 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); } From f84fe88c2ee7bec01e3d316bd6be61e1061a7e05 Mon Sep 17 00:00:00 2001 From: "edwin.fullard" Date: Fri, 11 May 2018 20:40:15 +0200 Subject: [PATCH 14/20] Update python bot to work with new building stats property --- starter-bots/python3/StarterBot.py | 36 +++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) 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() From cf74956623ab3a54a9e46b8545c57dcc14af5fba Mon Sep 17 00:00:00 2001 From: DaanKeun <34154910+DaanKeun@users.noreply.github.com> Date: Fri, 11 May 2018 20:56:15 +0200 Subject: [PATCH 15/20] Fix reference to unit rather than building --- game-rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.** From e8878b0d91993bad66ca1aa3bddb8880b5488b49 Mon Sep 17 00:00:00 2001 From: Pierre Roux Date: Fri, 11 May 2018 21:15:37 +0200 Subject: [PATCH 16/20] Replace starter bots with new bot --- starter-pack/config.json | 7 +- starter-pack/game-config.properties | 45 +++ starter-pack/makefile | 4 +- starter-pack/reference-bot/java/pom.xml | 2 +- .../java/za/co/entelect/challenge/Bot.java | 126 +++---- .../java/za/co/entelect/challenge/Main.java | 6 +- .../challenge/entities/BuildingStats.java | 15 + .../challenge/entities/GameDetails.java | 5 +- starter-pack/run.bat | 2 +- .../starter-bots/cplusplus/Readme.txt | 24 -- starter-pack/starter-bots/cplusplus/bot.json | 8 - .../starter-bots/cplusplus/compile.bat | 1 - .../starter-bots/cplusplus/samplebot.cpp | 314 ------------------ .../starter-bots/csharpcore/.gitignore | 33 -- .../starter-bots/csharpcore/StarterBot.sln | 25 -- .../starter-bots/csharpcore/StarterBot/Bot.cs | 152 --------- .../StarterBot/Entities/Building.cs | 18 - .../csharpcore/StarterBot/Entities/Cell.cs | 12 - .../StarterBot/Entities/CellStateContainer.cs | 14 - .../StarterBot/Entities/GameDetails.cs | 13 - .../StarterBot/Entities/GameState.cs | 11 - .../csharpcore/StarterBot/Entities/Missile.cs | 8 - .../csharpcore/StarterBot/Entities/Player.cs | 13 - .../StarterBot/Enums/BuildingType.cs | 18 - .../csharpcore/StarterBot/Enums/Direction.cs | 8 - .../csharpcore/StarterBot/Enums/PlayerType.cs | 8 - .../csharpcore/StarterBot/Program.cs | 22 -- .../csharpcore/StarterBot/StarterBot.csproj | 12 - .../csharpcore/StarterBot/state.json | 1 - starter-pack/starter-bots/csharpcore/bot.json | 8 - starter-pack/starter-bots/golang/bot.json | 8 + starter-pack/starter-bots/haskell/app/Main.hs | 10 + starter-pack/starter-bots/haskell/bot.json | 8 + .../starter-bots/haskell/package.yaml | 54 +++ starter-pack/starter-bots/java/.gitignore | 54 --- starter-pack/starter-bots/java/bot.json | 8 - starter-pack/starter-bots/java/pom.xml | 55 --- .../java/za/co/entelect/challenge/Bot.java | 190 ----------- .../java/za/co/entelect/challenge/Main.java | 46 --- .../entelect/challenge/entities/Building.java | 18 - .../co/entelect/challenge/entities/Cell.java | 36 -- .../entities/CellStateContainer.java | 31 -- .../challenge/entities/GameDetails.java | 13 - .../challenge/entities/GameState.java | 29 -- .../entelect/challenge/entities/Missile.java | 6 - .../entelect/challenge/entities/Player.java | 11 - .../challenge/enums/BuildingType.java | 22 -- .../entelect/challenge/enums/Direction.java | 16 - .../entelect/challenge/enums/PlayerType.java | 6 - .../starter-bots/javascript/StarterBot.js | 174 ---------- starter-pack/starter-bots/javascript/bot.json | 8 - starter-pack/starter-bots/kotlin/.gitignore | 54 --- starter-pack/starter-bots/kotlin/bot.json | 8 - starter-pack/starter-bots/kotlin/pom.xml | 129 ------- .../kotlin/za/co/entelect/challenge/Bot.kt | 129 ------- .../za/co/entelect/challenge/Entities.kt | 45 --- .../kotlin/za/co/entelect/challenge/Enums.kt | 25 -- .../kotlin/za/co/entelect/challenge/Main.kt | 41 --- starter-pack/starter-bots/php/bot.json | 8 + starter-pack/starter-bots/php/include/Bot.php | 79 +++++ .../starter-bots/php/include/GameState.php | 98 ++++++ starter-pack/starter-bots/php/include/Map.php | 119 +++++++ starter-pack/starter-bots/python3/README.md | 0 .../starter-bots/python3/StarterBot.py | 265 --------------- starter-pack/starter-bots/python3/bot.json | 8 - starter-pack/starter-bots/rust/.gitignore | 10 - starter-pack/starter-bots/rust/Cargo.toml | 10 - starter-pack/starter-bots/rust/README.md | 38 --- starter-pack/starter-bots/rust/bot.json | 8 - starter-pack/starter-bots/rust/src/main.rs | 168 ---------- starter-pack/starter-bots/rust/src/state.rs | 86 ----- ...0.1.jar => tower-defence-runner-1.1.1.jar} | Bin 5612074 -> 5613655 bytes 72 files changed, 528 insertions(+), 2538 deletions(-) create mode 100644 starter-pack/game-config.properties create mode 100644 starter-pack/reference-bot/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java delete mode 100644 starter-pack/starter-bots/cplusplus/Readme.txt delete mode 100644 starter-pack/starter-bots/cplusplus/bot.json delete mode 100644 starter-pack/starter-bots/cplusplus/compile.bat delete mode 100644 starter-pack/starter-bots/cplusplus/samplebot.cpp delete mode 100644 starter-pack/starter-bots/csharpcore/.gitignore delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot.sln delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Cell.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameState.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Program.cs delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj delete mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/state.json delete mode 100644 starter-pack/starter-bots/csharpcore/bot.json create mode 100644 starter-pack/starter-bots/golang/bot.json create mode 100644 starter-pack/starter-bots/haskell/app/Main.hs create mode 100644 starter-pack/starter-bots/haskell/bot.json create mode 100644 starter-pack/starter-bots/haskell/package.yaml delete mode 100644 starter-pack/starter-bots/java/.gitignore delete mode 100644 starter-pack/starter-bots/java/bot.json delete mode 100644 starter-pack/starter-bots/java/pom.xml delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Cell.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java delete mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java delete mode 100644 starter-pack/starter-bots/javascript/StarterBot.js delete mode 100644 starter-pack/starter-bots/javascript/bot.json delete mode 100644 starter-pack/starter-bots/kotlin/.gitignore delete mode 100644 starter-pack/starter-bots/kotlin/bot.json delete mode 100644 starter-pack/starter-bots/kotlin/pom.xml delete mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt delete mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt delete mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt delete mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt create mode 100644 starter-pack/starter-bots/php/bot.json create mode 100644 starter-pack/starter-bots/php/include/Bot.php create mode 100644 starter-pack/starter-bots/php/include/GameState.php create mode 100644 starter-pack/starter-bots/php/include/Map.php delete mode 100644 starter-pack/starter-bots/python3/README.md delete mode 100644 starter-pack/starter-bots/python3/StarterBot.py delete mode 100644 starter-pack/starter-bots/python3/bot.json delete mode 100644 starter-pack/starter-bots/rust/.gitignore delete mode 100644 starter-pack/starter-bots/rust/Cargo.toml delete mode 100644 starter-pack/starter-bots/rust/README.md delete mode 100644 starter-pack/starter-bots/rust/bot.json delete mode 100644 starter-pack/starter-bots/rust/src/main.rs delete mode 100644 starter-pack/starter-bots/rust/src/state.rs rename starter-pack/{tower-defence-runner-1.0.1.jar => tower-defence-runner-1.1.1.jar} (94%) 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/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/Readme.txt b/starter-pack/starter-bots/cplusplus/Readme.txt deleted file mode 100644 index 81ef64c..0000000 --- a/starter-pack/starter-bots/cplusplus/Readme.txt +++ /dev/null @@ -1,24 +0,0 @@ -Requires MinGW -https://nuwen.net/files/mingw/mingw-15.4.exe is a nicely maintained distro with a few other tools but there are others. - -Extracted the above files to C:\ - -compile.bat needs to be updated to your local MinGW path - -C++ Source code: samplebot.cpp -Compile with : compile.bat -Run bot : samplebot.exe - -I'm using the TextMap.txt, haven't used json before. - -Assumptions - - No inputs are given to the bot at runtime - - The working directory contains TextMap.txt - - PlayerCommand.txt will be written to the working directory - -Still to do - - Remove random moves and add simple logic - - Read in from json file - -bot.json file - - might need to have values for compile and running diff --git a/starter-pack/starter-bots/cplusplus/bot.json b/starter-pack/starter-bots/cplusplus/bot.json deleted file mode 100644 index c98b6f3..0000000 --- a/starter-pack/starter-bots/cplusplus/bot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "author": "John Doe", - "email": "john.doe@example.com", - "nickName": "Bjarne", - "botLocation": "/", - "botFileName": "samplebot", - "botLanguage": "c++" -} \ No newline at end of file diff --git a/starter-pack/starter-bots/cplusplus/compile.bat b/starter-pack/starter-bots/cplusplus/compile.bat deleted file mode 100644 index 2b76842..0000000 --- a/starter-pack/starter-bots/cplusplus/compile.bat +++ /dev/null @@ -1 +0,0 @@ -C:\MinGW\bin\g++.exe -Wall samplebot.cpp -o samplebot.exe \ No newline at end of file diff --git a/starter-pack/starter-bots/cplusplus/samplebot.cpp b/starter-pack/starter-bots/cplusplus/samplebot.cpp deleted file mode 100644 index 68a9383..0000000 --- a/starter-pack/starter-bots/cplusplus/samplebot.cpp +++ /dev/null @@ -1,314 +0,0 @@ -#include //not needed but useful for debugging -#include -#include -#include //for random number generation -#include //seeding random number generation -using namespace std; - -//maximum size for game state - Unknown -const int MAX_MAP_WIDTH = 100; -const int MAX_MAP_HEIGHT = 100; - -class BuildingInfo -{ - public: - string type; - int energyCost; - - //other possible info - int constructionTime; - int health; - int weaponCooldownTime; - int weaponDamage; - int energyGenerated; - - BuildingInfo(){} - - BuildingInfo(string in_type,int in_energyCost) - { - type = in_type; - energyCost = in_energyCost; - } -}; - -class Building -{ - public: - char buildingType; - char owner; - int constructionTimeLeft; - int health; - int weaponCooldownTimeLeft; - int weaponDamage; - int energyGeneratedPerTurn; - - Building() - { - buildingType = 'N'; - } -}; - -class Missile -{ - public: - char owner; - int damage; - - Missile() - {} - - Missile(char in_owner,int in_damage) - { - owner = in_owner; - damage = in_damage; - } - -}; - -class Player -{ - public: - char owner; - int energy; - int health; - int hitsTaken; - int score; - - Player() - {} -}; - -class Cell -{ - public: - char owner; - Building building; - vector missile; - - Cell(){} -}; - -class Gamestate -{ - public: - int roundNumber; - int mapWidth; - int mapHeight; - - vector buildingInfo; - - Player player[2]; - - Cell cell[MAX_MAP_WIDTH][MAX_MAP_HEIGHT]; - - //unknown values - int baseEnergyGenerated; - int maxRounds; - - Gamestate() - { - //setting guessed values - baseEnergyGenerated = 5; - maxRounds = 1000; - } - - //reads gamestate from TextMap.txt - void readinText() - { - string inputline; - ifstream statefile ("textMap.txt"); - - while ( getline(statefile,inputline) ) - { - //start of game info input - if(inputline == "XXXXXXXXX GAME INFO XXXXXXXXX") - { - getline(statefile,inputline); - roundNumber = stoi(inputline.substr(15)); - - getline(statefile,inputline); - mapWidth = stoi(inputline.substr(12)); - - getline(statefile,inputline); - mapHeight = stoi(inputline.substr(13)); - - getline(statefile,inputline); - } - - //start of building info input - if(inputline == "****** BUILDING PRICES ******") - { - getline(statefile,inputline); - - while(inputline != "*****************************") - { - string building_type = inputline.substr(0,inputline.find(":")-1); - int building_energyCost = stoi(inputline.substr(inputline.find(":")+2)); - - buildingInfo.push_back(BuildingInfo(building_type,building_energyCost)); - - getline(statefile,inputline); - } - } - - //start of player A inputs - if(inputline == "---------- PLAYER A ----------") - { - player[0].owner = 'A'; - - getline(statefile,inputline); - player[0].energy = stoi(inputline.substr(9)); - - getline(statefile,inputline); - player[0].health = stoi(inputline.substr(9)); - - getline(statefile,inputline); - player[0].hitsTaken = stoi(inputline.substr(12)); - - getline(statefile,inputline); - player[0].score = stoi(inputline.substr(8)); - - getline(statefile,inputline); - } - - //start of player B inputs - if(inputline == "---------- PLAYER B ----------") - { - player[1].owner = 'B'; - - getline(statefile,inputline); - player[1].energy = stoi(inputline.substr(9)); - - getline(statefile,inputline); - player[1].health = stoi(inputline.substr(9)); - - getline(statefile,inputline); - player[1].hitsTaken = stoi(inputline.substr(12)); - - getline(statefile,inputline); - player[1].score = stoi(inputline.substr(8)); - - getline(statefile,inputline); - } - - //start of map input - if(inputline == "############# MAP #############") - { - for(int map_y = 0;map_y < mapHeight;map_y++) - { - getline(statefile,inputline); - for(int map_x = 0;map_x < mapWidth;map_x++) - { - cell[map_x][map_y].building.buildingType = inputline[map_x*11 + 5]; - cell[map_x][map_y].owner = (map_x * 2 < mapWidth ? 'A' : 'B'); - } - } - } - - //start of building data - if(inputline == "######## BUILDING DATA #########") - { - getline(statefile,inputline);//FORMAT... - getline(statefile,inputline);//blank line - getline(statefile,inputline);//first building info - while(inputline != "###############################") - { - inputline = inputline.substr(inputline.find("[")+1); - int map_x = stoi(inputline.substr(0,inputline.find(","))); - inputline = inputline.substr(inputline.find(",")+1); - int map_y = stoi(inputline.substr(0,inputline.find("]"))); - inputline = inputline.substr(inputline.find(" ")+1); - - cell[map_x][map_y].building.owner = inputline[0]; - inputline = inputline.substr(inputline.find("|")+1); - - cell[map_x][map_y].building.constructionTimeLeft = stoi(inputline.substr(0,inputline.find("|"))); - inputline = inputline.substr(inputline.find("|")+1); - - cell[map_x][map_y].building.health = stoi(inputline.substr(0,inputline.find("|"))); - inputline = inputline.substr(inputline.find("|")+1); - - cell[map_x][map_y].building.weaponCooldownTimeLeft = stoi(inputline.substr(0,inputline.find("|"))); - inputline = inputline.substr(inputline.find("|")+1); - - cell[map_x][map_y].building.weaponDamage = stoi(inputline.substr(0,inputline.find("|"))); - inputline = inputline.substr(inputline.find("|")+1); - - cell[map_x][map_y].building.energyGeneratedPerTurn = stoi(inputline); - - getline(statefile,inputline); - } - } - - //start of missile data - if(inputline == "####### MISSILE DATA ########") - { - getline(statefile,inputline);//FORMAT... - getline(statefile,inputline);//blank line - getline(statefile,inputline);//first missile info - while(inputline != "###############################") - { - inputline = inputline.substr(inputline.find("[")+1); - int map_x = stoi(inputline.substr(0,inputline.find(","))); - inputline = inputline.substr(inputline.find(",")+1); - int map_y = stoi(inputline.substr(0,inputline.find("]"))); - inputline = inputline.substr(inputline.find(" ")+1); - - char missile_owner = inputline[0]; - inputline = inputline.substr(inputline.find("|")+1); - int missile_damage = stoi(inputline); - - cell[map_x][map_y].missile.push_back(Missile(missile_owner,missile_damage)); - - getline(statefile,inputline); - } - } - } - - statefile.close(); - } - - vector possible_moves() - { - vector moves; - moves.push_back(""); - - for(int x = 0;x= buildingInfo[building_type].energyCost) - { - moves.push_back(to_string(x)+","+to_string(y)+","+to_string(building_type)); - } - } - } - } - - return moves; - } -}; - -int main() -{ - //initialize random seed - srand (time(NULL)); - - //create gamestate object - Gamestate gamestate; - gamestate.readinText(); - - //get legal moves rom gamestate - vector possible_moves = gamestate.possible_moves(); - - //write random move to command file - ofstream movefile ("command.txt"); - movefile << possible_moves[rand()%possible_moves.size()]; - movefile.close(); - - return 0; -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/.gitignore b/starter-pack/starter-bots/csharpcore/.gitignore deleted file mode 100644 index 1174d4b..0000000 --- a/starter-pack/starter-bots/csharpcore/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -*.swp -*.*~ -project.lock.json -.DS_Store -*.pyc -nupkg/ - -# Visual Studio Code -.vscode - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ -msbuild.log -msbuild.err -msbuild.wrn - -# Visual Studio 2017 -.vs/ \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot.sln b/starter-pack/starter-bots/csharpcore/StarterBot.sln deleted file mode 100644 index 70410ed..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2005 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StarterBot", "StarterBot\StarterBot.csproj", "{5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {88D3E82E-F22C-4F42-A571-DEA3EB1D830E} - EndGlobalSection -EndGlobal diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs deleted file mode 100644 index 883411b..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using StarterBot.Entities; -using StarterBot.Enums; - -namespace StarterBot -{ - public class Bot - { - private readonly GameState _gameState; - private readonly int _attackCost; - private readonly int _defenseCost; - private readonly int _energyCost; - private readonly int _mapWidth; - private readonly int _mapHeight; - private readonly Player _player; - private readonly Random _random; - - 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._random = new Random((int) DateTime.Now.Ticks); - - _player = gameState.Players.Single(x => x.PlayerType == PlayerType.A); - } - - 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) - { - return commandToReturn; - } - - //Get all opponent attack buildings - var opponentAttackBuildings = GetBuildings(PlayerType.B, BuildingType.Attack); - - //Get all my attack buildings - var myAttackBuildings = GetBuildings(PlayerType.A, BuildingType.Attack); - //Get all my defense buildings - var myDefenseBuildings = GetBuildings(PlayerType.A, BuildingType.Defense); - - var myBuildings = new List(); - - //Combine all buildings into a single list - myBuildings.AddRange(myAttackBuildings); - myBuildings.AddRange(myDefenseBuildings); - - //Do a random command if enemy has no attack buildings to defend against - if (!opponentAttackBuildings.Any()) - { - commandToReturn = GetRandomCommand(myBuildings); - } - else - { - //Get all rows with enemy buildings where I don't have a defense building - var rows = GetEnemyBuildingRows(opponentAttackBuildings, myDefenseBuildings); - - //Place defense building randomly in first row from list - if (rows.Count > 0) - { - commandToReturn = GetValidAttackCommand(rows[0], myBuildings); - } - else - { - commandToReturn = GetRandomCommand(myBuildings); - } - } - - return commandToReturn; - } - - //Get a valid attack command for a row - public string GetValidAttackCommand(int yCoordinate, List myBuildings) - { - var xRandom = _random.Next(_mapWidth / 2); - - while (myBuildings.Any(x => x.X == xRandom && x.Y == yCoordinate && x.Buildings.Any())) - { - xRandom = _random.Next(_mapWidth / 2); - } - - return $"{xRandom},{yCoordinate},{(int)BuildingType.Defense}"; - } - - //Get random valid command - public string GetRandomCommand(List myBuildings) - { - //Place building randomly on my half of the map - var xRandom = _random.Next(_mapWidth / 2); - var yRandom = _random.Next(_mapHeight); - var btRandom = _random.Next(Enum.GetNames(typeof(BuildingType)).Length); - - while (myBuildings.Any(x => x.X == xRandom && x.Y == yRandom && x.Buildings.Any())) - { - xRandom = _random.Next(_mapWidth / 2); - yRandom = _random.Next(_mapHeight); - } - - return $"{xRandom},{yRandom},{btRandom}"; - } - - //Get all rows which contain an enemy building where I don't have a defense building - private List GetEnemyBuildingRows(List opponentAttackBuildings, - List myDefenseBuildings) - { - var rows = new List(); - foreach (var enemyAttackBuilding in opponentAttackBuildings) - { - if (rows.Contains(enemyAttackBuilding.Y)) continue; - rows.Add(enemyAttackBuilding.Y); - } - - foreach (var myDefenseBuilding in myDefenseBuildings) - { - if (rows.Contains(myDefenseBuilding.Y)) - { - rows.Remove(myDefenseBuilding.Y); - } - } - - return rows; - } - - //Get buildings of a certain type for a certain player - private List GetBuildings(PlayerType playerType, BuildingType buildingType) - { - var list = new List(); - foreach (var cellStateContainers in _gameState.GameMap) - { - foreach (var cellStateContainer in cellStateContainers) - { - if (cellStateContainer.CellOwner == playerType && - cellStateContainer.Buildings.Any(x => x.BuildingType == buildingType)) - { - list.Add(cellStateContainer); - } - } - } - - return list; - } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs deleted file mode 100644 index 858a286..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs +++ /dev/null @@ -1,18 +0,0 @@ -using StarterBot.Enums; - -namespace StarterBot.Entities -{ - public class Building - { - public int Health { get; set; } - public int ConstructionTimeLeft { get; set; } - public int Price { get; set; } - public int WeaponDamage { get; set; } - public int WeaponSpeed { get; set; } - public int WeaponCooldownTimeLeft { get; set; } - public int WeaponCooldownPeriod { get; set; } - public int DestroyScore { get; set; } - public int EnergyGeneratedPerTurn { get; set; } - public BuildingType BuildingType { get; set; } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Cell.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Cell.cs deleted file mode 100644 index e94fcbc..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Cell.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; -using StarterBot.Enums; - -namespace StarterBot.Entities -{ - public class Cell - { - public int X { get; set; } - public int Y { get; set; } - public PlayerType PlayerType { get; set; } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs deleted file mode 100644 index 6fff864..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using StarterBot.Enums; - -namespace StarterBot.Entities -{ - public class CellStateContainer - { - public int X { get; set; } - public int Y { get; set; } - public PlayerType CellOwner { get; set; } - public List Buildings { get; set; } - public List Missiles { get; set; } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs deleted file mode 100644 index 447374b..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using StarterBot.Enums; - -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; } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameState.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameState.cs deleted file mode 100644 index 1be809c..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameState.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace StarterBot.Entities -{ - public class GameState - { - public List Players { get; set; } - public CellStateContainer[][] GameMap { get; set; } - public GameDetails GameDetails { get; set; } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs deleted file mode 100644 index 244891c..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StarterBot.Entities -{ - public class Missile - { - public int Damage { get; set; } - public int Speed { get; set; } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs deleted file mode 100644 index 5af0439..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs +++ /dev/null @@ -1,13 +0,0 @@ -using StarterBot.Enums; - -namespace StarterBot.Entities -{ - public class Player - { - public PlayerType PlayerType { get; set; } - public int Energy { get; set; } - public int Health { get; set; } - public int HitsTaken { get; set; } - public int Score { get; set; } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs deleted file mode 100644 index ea5ae04..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; - -namespace StarterBot.Enums -{ - public enum BuildingType - { - [JsonProperty("DEFENSE")] - [EnumMember(Value = "DEFENSE")] - Defense = 0, - [JsonProperty("ATTACK")] - [EnumMember(Value = "ATTACK")] - Attack = 1, - [JsonProperty("ENERGY")] - [EnumMember(Value = "ENERGY")] - Energy = 2 - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs deleted file mode 100644 index 2f0b743..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StarterBot.Enums -{ - public enum Direction - { - Left = -1, - Right = 1, - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs deleted file mode 100644 index 1fbff88..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StarterBot.Enums -{ - public enum PlayerType - { - A, - B - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Program.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Program.cs deleted file mode 100644 index 730852a..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; -using StarterBot.Entities; - -namespace StarterBot -{ - public class Program - { - private static string _commandFileName = "command.txt"; - private static string _stateFileName = "state.json"; - - static void Main(string[] args) - { - var gameState = JsonConvert.DeserializeObject(File.ReadAllText(_stateFileName)); - - var command = new Bot(gameState).Run(); - - File.WriteAllText(_commandFileName, command); - } - } -} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj b/starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj deleted file mode 100644 index 9c432fa..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - netcoreapp2.0 - - - - - - - diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/state.json b/starter-pack/starter-bots/csharpcore/StarterBot/state.json deleted file mode 100644 index a86a636..0000000 --- a/starter-pack/starter-bots/csharpcore/StarterBot/state.json +++ /dev/null @@ -1 +0,0 @@ -{"gameDetails":{"round":11,"mapWidth":8,"mapHeight":4,"buildingPrices":{"DEFENSE":30,"ENERGY":20,"ATTACK":30}},"players":[{"playerType":"A","energy":30,"health":100,"hitsTaken":0,"score":51},{"playerType":"B","energy":30,"health":100,"hitsTaken":0,"score":51}],"gameMap":[[{"x":0,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":1,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":4,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":5,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"}],[{"x":0,"y":1,"buildings":[{"health":5,"constructionTimeLeft":-1,"price":30,"weaponDamage":5,"weaponSpeed":1,"weaponCooldownTimeLeft":1,"weaponCooldownPeriod":3,"destroyMultiplier":1,"constructionScore":1,"energyGeneratedPerTurn":0,"buildingType":"ATTACK","x":0,"y":1,"playerType":"A"}],"missiles":[],"cellOwner":"A"},{"x":1,"y":1,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":1,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":1,"buildings":[],"missiles":[{"damage":5,"speed":1,"x":3,"y":1,"playerType":"A"}],"cellOwner":"A"},{"x":4,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":5,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"}],[{"x":0,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":1,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":4,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":5,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"}],[{"x":0,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":1,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":4,"y":3,"buildings":[],"missiles":[{"damage":5,"speed":1,"x":4,"y":3,"playerType":"B"}],"cellOwner":"B"},{"x":5,"y":3,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":3,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":3,"buildings":[{"health":5,"constructionTimeLeft":-1,"price":30,"weaponDamage":5,"weaponSpeed":1,"weaponCooldownTimeLeft":1,"weaponCooldownPeriod":3,"destroyMultiplier":1,"constructionScore":1,"energyGeneratedPerTurn":0,"buildingType":"ATTACK","x":7,"y":3,"playerType":"B"}],"missiles":[],"cellOwner":"B"}]]} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/bot.json b/starter-pack/starter-bots/csharpcore/bot.json deleted file mode 100644 index 1d044cc..0000000 --- a/starter-pack/starter-bots/csharpcore/bot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "author": "John Doe", - "email": "john.doe@example.com", - "nickName": "Bill", - "botLocation": "/StarterBot/bin/Debug/netcoreapp2.0", - "botFileName": "StarterBot.dll", - "botLanguage": "c#core" -} \ No newline at end of file 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/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/java/.gitignore b/starter-pack/starter-bots/java/.gitignore deleted file mode 100644 index 68d1895..0000000 --- a/starter-pack/starter-bots/java/.gitignore +++ /dev/null @@ -1,54 +0,0 @@ -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff: -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -# Gradle: -.idea/**/gradle.xml -.idea/**/libraries - -# CMake -cmake-build-debug/ -cmake-build-release/ - -# Mongo Explorer plugin: -.idea/**/mongoSettings.xml - -## File-based project format: -*.iws - -## Plugin-specific files: - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -\.idea/ -*.iml - -target/* diff --git a/starter-pack/starter-bots/java/bot.json b/starter-pack/starter-bots/java/bot.json deleted file mode 100644 index 072dc30..0000000 --- a/starter-pack/starter-bots/java/bot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "author": "John Doe", - "email": "john.doe@example.com", - "nickName": "James", - "botLocation": "/target", - "botFileName": "java-sample-bot-1.0-SNAPSHOT-jar-with-dependencies.jar", - "botLanguage": "java" -} \ No newline at end of file diff --git a/starter-pack/starter-bots/java/pom.xml b/starter-pack/starter-bots/java/pom.xml deleted file mode 100644 index eb5be9e..0000000 --- a/starter-pack/starter-bots/java/pom.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - 4.0.0 - - za.co.entelect.challenge - java-sample-bot - 1.0-SNAPSHOT - - - - org.apache.maven.plugins - maven-compiler-plugin - - 8 - 8 - - - - maven-assembly-plugin - - - - za.co.entelect.challenge.Main - - - - jar-with-dependencies - - - - - make-assembly - package - - single - - - - - - - - - com.google.code.gson - gson - 2.8.2 - - - - - 1.8 - - \ No newline at end of file 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 deleted file mode 100644 index 772b711..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java +++ /dev/null @@ -1,190 +0,0 @@ -package za.co.entelect.challenge; - -import za.co.entelect.challenge.entities.*; -import za.co.entelect.challenge.enums.BuildingType; -import za.co.entelect.challenge.enums.PlayerType; - -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import static za.co.entelect.challenge.enums.BuildingType.ATTACK; -import static za.co.entelect.challenge.enums.BuildingType.DEFENSE; - -public class Bot { - private static final String NOTHING_COMMAND = ""; - private GameState gameState; - private GameDetails gameDetails; - private int gameWidth; - private int gameHeight; - private Player myself; - private Player opponent; - private List buildings; - private List missiles; - - /** - * Constructor - * - * @param gameState the game state - **/ - public Bot(GameState gameState) { - this.gameState = gameState; - gameDetails = gameState.getGameDetails(); - gameWidth = gameDetails.mapWidth; - gameHeight = gameDetails.mapHeight; - myself = gameState.getPlayers().stream().filter(p -> p.playerType == PlayerType.A).findFirst().get(); - opponent = gameState.getPlayers().stream().filter(p -> p.playerType == PlayerType.B).findFirst().get(); - - buildings = gameState.getGameMap().stream() - .flatMap(c -> c.getBuildings().stream()) - .collect(Collectors.toList()); - - missiles = gameState.getGameMap().stream() - .flatMap(c -> c.getMissiles().stream()) - .collect(Collectors.toList()); - } - - /** - * Run - * - * @return the result - **/ - public String run() { - if (isUnderAttack()) { - return defendRow(); - } else if (hasEnoughEnergyForMostExpensiveBuilding()) { - return buildRandom(); - } else { - return doNothingCommand(); - } - } - - /** - * Build random building - * - * @return the result - **/ - private String buildRandom() { - List emptyCells = gameState.getGameMap().stream() - .filter(c -> c.getBuildings().size() == 0 && c.x < (gameWidth / 2)) - .collect(Collectors.toList()); - - if (emptyCells.isEmpty()) { - return doNothingCommand(); - } - - CellStateContainer randomEmptyCell = getRandomElementOfList(emptyCells); - BuildingType randomBuildingType = getRandomElementOfList(Arrays.asList(BuildingType.values())); - - if (!canAffordBuilding(randomBuildingType)) { - return doNothingCommand(); - } - - return randomBuildingType.buildCommand(randomEmptyCell.x, randomEmptyCell.y); - } - - /** - * Has enough energy for most expensive building - * - * @return the result - **/ - private boolean hasEnoughEnergyForMostExpensiveBuilding() { - return gameDetails.buildingPrices.values().stream() - .filter(bp -> bp < myself.energy) - .toArray().length == 3; - } - - /** - * Defend row - * - * @return the result - **/ - private String defendRow() { - for (int i = 0; i < gameHeight; i++) { - boolean opponentAttacking = getAnyBuildingsForPlayer(PlayerType.B, b -> b.buildingType == ATTACK, i); - if (opponentAttacking && canAffordBuilding(DEFENSE)) { - return placeBuildingInRow(DEFENSE, i); - } - } - - return buildRandom(); - } - - /** - * Checks if this is under attack - * - * @return true if this is under attack - **/ - private boolean isUnderAttack() { - //if enemy has an attack building and i dont have a blocking wall - for (int i = 0; i < gameHeight; i++) { - boolean opponentAttacks = getAnyBuildingsForPlayer(PlayerType.B, building -> building.buildingType == ATTACK, i); - boolean myDefense = getAnyBuildingsForPlayer(PlayerType.A, building -> building.buildingType == DEFENSE, i); - - if (opponentAttacks && !myDefense) { - return true; - } - } - return false; - } - - /** - * Do nothing command - * - * @return the result - **/ - private String doNothingCommand() { - return NOTHING_COMMAND; - } - - /** - * Place building in row - * - * @param buildingType the building type - * @param y the y - * @return the result - **/ - private String placeBuildingInRow(BuildingType buildingType, int y) { - List emptyCells = gameState.getGameMap().stream() - .filter(c -> c.getBuildings().isEmpty() - && c.y == y - && c.x < (gameWidth / 2) - 1) - .collect(Collectors.toList()); - - if (emptyCells.isEmpty()) { - return buildRandom(); - } - - CellStateContainer randomEmptyCell = getRandomElementOfList(emptyCells); - return buildingType.buildCommand(randomEmptyCell.x, randomEmptyCell.y); - } - - /** - * Get random element of list - * - * @param list the list < t > - * @return the result - **/ - private T getRandomElementOfList(List list) { - return list.get((new Random()).nextInt(list.size())); - } - - private boolean getAnyBuildingsForPlayer(PlayerType playerType, Predicate filter, int y) { - return buildings.stream() - .filter(b -> b.getPlayerType() == playerType - && b.getY() == y) - .anyMatch(filter); - } - - /** - * Can afford building - * - * @param buildingType the building type - * @return the result - **/ - private boolean canAffordBuilding(BuildingType buildingType) { - return myself.energy >= gameDetails.buildingPrices.get(buildingType); - } -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java deleted file mode 100644 index 79ca286..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java +++ /dev/null @@ -1,46 +0,0 @@ -package za.co.entelect.challenge; - -import com.google.gson.Gson; -import za.co.entelect.challenge.entities.GameState; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Paths; - -public class Main { - private static final String COMMAND_FILE_NAME = "command.txt"; - private static final String STATE_FILE_NAME = "state.json"; - - /** - * 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) { - e.printStackTrace(); - } - - Gson gson = new Gson(); - GameState gameState = gson.fromJson(state, GameState.class); - - Bot bot = new Bot(gameState); - String command = bot.run(); - - writeBotResponseToFile(command); - } - - /** - * Write bot response to file - * @param command the command - **/ - private static void writeBotResponseToFile(String command) { - try { - Files.write(Paths.get(COMMAND_FILE_NAME), command.getBytes()); - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java deleted file mode 100644 index 49deaaf..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java +++ /dev/null @@ -1,18 +0,0 @@ -package za.co.entelect.challenge.entities; - -import za.co.entelect.challenge.enums.BuildingType; -import za.co.entelect.challenge.enums.PlayerType; - -public class Building extends Cell { - - public int health; - public int constructionTimeLeft; - public int price; - public int weaponDamage; - public int weaponSpeed; - public int weaponCooldownTimeLeft; - public int weaponCooldownPeriod; - public int destroyScore; - public int energyGeneratedPerTurn; - public BuildingType buildingType; -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Cell.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Cell.java deleted file mode 100644 index 12c8b9d..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Cell.java +++ /dev/null @@ -1,36 +0,0 @@ -package za.co.entelect.challenge.entities; - -import za.co.entelect.challenge.enums.PlayerType; - -public class Cell { - - protected int x; - protected int y; - protected PlayerType playerType; - - public Cell() { - - } - - public Cell(int x, int y, PlayerType playerType) { - this.x = x; - this.y = y; - this.playerType = playerType; - } - - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public PlayerType getPlayerType() { - return playerType; - } - - public boolean isPlayers(PlayerType id) { - return id == playerType; - } -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java deleted file mode 100644 index 0b42fc0..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java +++ /dev/null @@ -1,31 +0,0 @@ -package za.co.entelect.challenge.entities; - -import za.co.entelect.challenge.enums.PlayerType; - -import java.util.ArrayList; -import java.util.List; - -public class CellStateContainer { - public int x; - public int y; - public PlayerType cellOwner; - protected List buildings; - protected List missiles; - - public CellStateContainer(int x, int y, PlayerType cellOwner) { - this.x = x; - this.y = y; - this.cellOwner = cellOwner; - this.buildings = new ArrayList<>(); - this.missiles = new ArrayList<>(); - } - - public List getBuildings() { - return this.buildings; - } - - public List getMissiles() { - return this.missiles; - } - -} 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 deleted file mode 100644 index 565682d..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java +++ /dev/null @@ -1,13 +0,0 @@ -package za.co.entelect.challenge.entities; - -import za.co.entelect.challenge.enums.BuildingType; - -import java.util.HashMap; - -public class GameDetails { - public int round; - public int mapWidth; - public int mapHeight; - public HashMap buildingPrices; - -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java deleted file mode 100644 index 2c4aa6c..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java +++ /dev/null @@ -1,29 +0,0 @@ -package za.co.entelect.challenge.entities; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class GameState { - protected Player[] players; - protected CellStateContainer[][] gameMap; - protected GameDetails gameDetails; - - public List getPlayers() { - return new ArrayList<>(Arrays.asList(players)); - } - - public List getGameMap() { - ArrayList list = new ArrayList<>(); - - for (CellStateContainer[] aGameMap : gameMap) { - list.addAll(Arrays.asList(aGameMap)); - } - - return list; - } - - public GameDetails getGameDetails() { - return gameDetails; - } -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java deleted file mode 100644 index 344cbd2..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java +++ /dev/null @@ -1,6 +0,0 @@ -package za.co.entelect.challenge.entities; - -public class Missile extends Cell { - private int damage; - private int speed; -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java deleted file mode 100644 index a535f44..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java +++ /dev/null @@ -1,11 +0,0 @@ -package za.co.entelect.challenge.entities; - -import za.co.entelect.challenge.enums.PlayerType; - -public class Player { - public PlayerType playerType; - public int energy; - public int health; - public int hitsTaken; - public int score; -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java deleted file mode 100644 index 4dc788a..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java +++ /dev/null @@ -1,22 +0,0 @@ -package za.co.entelect.challenge.enums; - -public enum BuildingType { - DEFENSE(0), - ATTACK(1), - ENERGY(2); - - private int type; - - BuildingType(int type) { - - this.type = type; - } - - public int getType() { - return type; - } - - public String buildCommand(int x, int y) { - return String.format("%d,%d,%d", x, y, getType()); - } -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java deleted file mode 100644 index 70275ce..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java +++ /dev/null @@ -1,16 +0,0 @@ -package za.co.entelect.challenge.enums; - -public enum Direction { - LEFT(-1), - RIGHT(1); - - private int multiplier; - - Direction(int multiplier) { - this.multiplier = multiplier; - } - - public int getMultiplier() { - return multiplier; - } -} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java deleted file mode 100644 index cf95ea4..0000000 --- a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java +++ /dev/null @@ -1,6 +0,0 @@ -package za.co.entelect.challenge.enums; - -public enum PlayerType { - A, - B -} diff --git a/starter-pack/starter-bots/javascript/StarterBot.js b/starter-pack/starter-bots/javascript/StarterBot.js deleted file mode 100644 index c142342..0000000 --- a/starter-pack/starter-bots/javascript/StarterBot.js +++ /dev/null @@ -1,174 +0,0 @@ -"use strict"; - -let fs = require('fs'); - -let commandFileName = "command.txt"; -let stateFileName = "state.json"; - -let key = ""; -let workingDirectory = ""; -let stateFile = ""; -let myself = ""; -let opponent = ""; -let gameMap = ""; -let mapSize = ""; -let cells = ""; -let buildings = ""; -let missiles = ""; -let buildingPrices = []; - -// Capture the arguments -initBot(process.argv.slice(2)); - -function initBot(args) { - key = args[0]; - workingDirectory = args[1]; - - // Read the current state and choose an action - stateFile = require('./' + stateFileName); - - myself = stateFile.players.filter(p => p.playerType === 'A')[0]; - opponent = stateFile.players.filter(p => p.playerType === 'B')[0]; - mapSize = { - x: stateFile.gameDetails.mapWidth, - y: stateFile.gameDetails.mapHeight - }; - - let prices = stateFile.gameDetails.buildingPrices; - buildingPrices[0]= prices.DEFENSE; - buildingPrices[1]= prices.ATTACK; - buildingPrices[2]= prices.ENERGY; - - gameMap = stateFile.gameMap; - initEntities(); - - runStrategy(); -} - -function initEntities() { - cells = flatMap(gameMap); // all cells on the entire map - - buildings = cells.filter(cell => cell.buildings.length > 0).map(cell => cell.buildings); - buildings = flatMap(buildings); // flat array of everyone's buildings - - missiles = cells.filter(cell => cell.missiles.length > 0).map(cell => cell.missiles); - missiles = flatMap(missiles); // flat array of everyone's missiles -} - -function runStrategy() { - if (isUnderAttack()) { - defendRow(); - } else if (hasEnoughEnergyForMostExpensiveBuilding()) { - buildRandom(); - } else { - doNothingCommand(); - } -} - -function isUnderAttack() { - // is there a row under attack? and have enough energy to build defence? - let myDefenders = buildings.filter(b => b.playerType == 'A' && b.buildingType == 'DEFENSE'); - 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]); -} - -function defendRow() { - // is there a row under attack? and have enough energy to build defence? - let myDefenders = buildings.filter(b => b.playerType == 'A' && b.buildingType == 'DEFENSE'); - let opponentAttackers = buildings.filter(b => b.playerType == 'B' && b.buildingType == 'ATTACK') - .filter(b => !myDefenders.some(d => d.y == b.y)); - if (opponentAttackers.length == 0) { - buildRandom(); - return - } - // choose the first row with an opponent attacker - let rowNumber = opponentAttackers[0].y; - // get all the x-coordinates for this row, that are empty - let emptyCells = cells.filter(c => c.buildings.length == 0 && c.x <= (mapSize.x / 2) - 1 && c.y == rowNumber); - if (emptyCells.length == 0) { - // cannot build there, try to build somewhere else - buildRandom(); - return - } - - let command = {x: '', y: '', bt: ''}; - command.x = getRandomFromArray(emptyCells).x; - command.y = rowNumber; - command.bt = 0; // defence building - buildCommand(command.x, command.y, command.bt); -} - -function hasEnoughEnergyForMostExpensiveBuilding() { - return (myself.energy >= Math.max(...buildingPrices)); -} - -function buildRandom() { - // cells without buildings on them, and on my half of the map - let emptyCells = cells.filter(c => c.buildings.length == 0 && c.x <= (mapSize.x / 2) - 1); - if (emptyCells.length == 0) { - doNothingCommand(); - return; - } - let randomCell = getRandomFromArray(emptyCells); - - let command = {x: '', y: '', bt: ''}; - command.x = randomCell.x; - command.y = randomCell.y; - command.bt = getRandomInteger(2); - buildCommand(command.x, command.y, command.bt); -} - -function buildCommand(x, y, bt) { - writeToFile(commandFileName, `${x},${y},${bt}`); -} - -function doNothingCommand() { - writeToFile(commandFileName, ``); -} - -function writeToFile(fileName, payload) { - fs.writeFile('./' + fileName, payload, function (err) { - if (err) { - return console.log(err); - } - // console.log(payload); - }); -} - -/*** - * Returns an array with one less level of nesting - * @param array - * @returns {Array} - */ -function flatMap(array) { - return array.reduce((acc, x) => acc.concat(x), []); -} - -/*** - * Returns a random integer between 0(inclusive) and max(inclusive) - * @param max - * @returns {number} - */ -function getRandomInteger(max) { - return Math.round(Math.random() * max); -} - -/** - * Returns an array that is filled with integers from 0(inclusive) to count(inclusive) - * @param count - * @returns {number[]} - */ -function getArrayRange(count) { - return Array.from({length: count}, (v, i) => i); -} - -/** - * Return a random element from a given array - * @param array - * @returns {*} - */ -function getRandomFromArray(array) { - return array[Math.floor((Math.random() * array.length))]; -} diff --git a/starter-pack/starter-bots/javascript/bot.json b/starter-pack/starter-bots/javascript/bot.json deleted file mode 100644 index c8f852b..0000000 --- a/starter-pack/starter-bots/javascript/bot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "author":"John Doe", - "email":"john.doe@example.com", - "nickName" :"Brendan", - "botLocation": "/", - "botFileName": "StarterBot.js", - "botLanguage": "javascript" -} \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/.gitignore b/starter-pack/starter-bots/kotlin/.gitignore deleted file mode 100644 index 68d1895..0000000 --- a/starter-pack/starter-bots/kotlin/.gitignore +++ /dev/null @@ -1,54 +0,0 @@ -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff: -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -# Gradle: -.idea/**/gradle.xml -.idea/**/libraries - -# CMake -cmake-build-debug/ -cmake-build-release/ - -# Mongo Explorer plugin: -.idea/**/mongoSettings.xml - -## File-based project format: -*.iws - -## Plugin-specific files: - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -\.idea/ -*.iml - -target/* diff --git a/starter-pack/starter-bots/kotlin/bot.json b/starter-pack/starter-bots/kotlin/bot.json deleted file mode 100644 index 4f8ecb2..0000000 --- a/starter-pack/starter-bots/kotlin/bot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "author": "John Doe", - "email": "john.doe@example.com", - "nickName": "Dmitry", - "botLocation": "/target", - "botFileName": "kotlin-sample-bot-1.0-SNAPSHOT-jar-with-dependencies.jar", - "botLanguage": "kotlin" -} \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/pom.xml b/starter-pack/starter-bots/kotlin/pom.xml deleted file mode 100644 index 752c214..0000000 --- a/starter-pack/starter-bots/kotlin/pom.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - 4.0.0 - - za.co.entelect.challenge - kotlin-sample-bot - 1.0-SNAPSHOT - - - 1.8 - 1.2.40 - - - - - - kotlin-maven-plugin - org.jetbrains.kotlin - ${kotlin.version} - - - compile - compile - - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/main/java - - - - - test-compile - test-compile - - - ${project.basedir}/src/test/kotlin - ${project.basedir}/src/test/java - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.5.1 - - - - default-compile - none - - - - default-testCompile - none - - - java-compile - compile - compile - - - java-test-compile - test-compile - testCompile - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.6 - - - - true - za.co.entelect.challenge.Main - - - - - - org.apache.maven.plugins - maven-assembly-plugin - 2.6 - - - make-assembly - package - single - - - - za.co.entelect.challenge.Main - - - - jar-with-dependencies - - - - - - - - - - - com.google.code.gson - gson - 2.8.2 - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - ${kotlin.version} - - - org.jetbrains.kotlin - kotlin-test - ${kotlin.version} - test - - - - \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt deleted file mode 100644 index 7d5cfb0..0000000 --- a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt +++ /dev/null @@ -1,129 +0,0 @@ -package za.co.entelect.challenge - -import java.util.ArrayList -import java.util.Random - -class Bot(private val gameState: GameState) { - private val gameDetails: GameDetails = gameState.gameDetails - private val gameWidth: Int - private val gameHeight: Int - private val myself: Player - private val opponent: Player - private val buildings: List - private val missiles: List - - //if enemy has an attack building and I don't have a blocking wall - private val isUnderAttack: Boolean - get() { - for (i in 0 until gameHeight) { - val opponentAttacksCount = getAllBuildingsForPlayer(PlayerType.B, ::isAttackBuilding, i).size - val myDefenseCount = getAllBuildingsForPlayer(PlayerType.A, ::isDefenceBuilding, i).size - if (opponentAttacksCount > 0 && myDefenseCount == 0) { - return true - } - } - return false - } - - init { - // energyPerTurn = gameDetails.; - gameWidth = gameDetails.mapWidth - gameHeight = gameDetails.mapHeight - myself = gameState.players.first { p -> p.playerType == PlayerType.A } - opponent = gameState.players.first { p -> p.playerType == PlayerType.B } - buildings = gameState.gameMap.flatten().map { cellStateContainer -> cellStateContainer.buildings }.flatten() - missiles = gameState.gameMap.flatten().map { cellStateContainer -> cellStateContainer.missiles }.flatten() - } - - fun run(): String { - return when { - isUnderAttack -> defendRow() - hasEnoughEnergyForMostExpensiveBuilding() -> buildRandom() - else -> doNothingCommand() - } - } - - private fun buildRandom(): String { - val emptyCells = gameState.gameMap.flatten() - .filter { c -> c.buildings.isEmpty() && c.x < gameWidth / 2 } - if (emptyCells.isEmpty()) { - return doNothingCommand() - } - val randomEmptyCell = getRandomElementOfList(emptyCells) - val buildingTypes = ArrayList(gameDetails.buildingPrices.keys) - val randomBuildingType = getRandomElementOfList(buildingTypes) - return if (!canAffordBuilding(randomBuildingType)) { - doNothingCommand() - } else buildCommand(randomEmptyCell.x, randomEmptyCell.y, randomBuildingType) - } - - private fun hasEnoughEnergyForMostExpensiveBuilding(): Boolean { - return gameDetails.buildingPrices.values.stream() - .filter { bp -> bp < myself.energy } - .toArray().size == 3 - } - - private fun defendRow(): String { - for (i in 0 until gameHeight) { - val opponentAttacksCount = getAllBuildingsForPlayer(PlayerType.B, { b -> b.buildingType == BuildingType.ATTACK }, i).size - if (opponentAttacksCount > 0 && canAffordBuilding(BuildingType.DEFENSE)) { - return placeBuildingInRow(BuildingType.DEFENSE, i) - } - } - return buildRandom() - } - - private fun doNothingCommand(): String { - return "" - } - - private fun placeBuildingInRow(buildingType: BuildingType, y: Int): String { - val emptyCells = gameState.gameMap.flatten() - .filter { - (it.buildings.isEmpty() - && it.y == y - && it.x < gameWidth / 2 - 1) - } - if (emptyCells.isEmpty()) { - return buildRandom() - } - val randomEmptyCell = getRandomElementOfList(emptyCells) - return buildCommand(randomEmptyCell.x, randomEmptyCell.y, buildingType) - } - - private fun getRandomElementOfList(list: List): T { - return list[Random().nextInt(list.size)] - } - - private fun buildCommand(x: Int, y: Int, buildingType: BuildingType): String { - val buildCommand = StringBuilder() - - buildCommand.append(x) - buildCommand.append(",") - buildCommand.append(y) - buildCommand.append(",") - when (buildingType) { - - BuildingType.DEFENSE -> buildCommand.append("0") - BuildingType.ATTACK -> buildCommand.append("1") - BuildingType.ENERGY -> buildCommand.append("2") - } - return buildCommand.toString() - } - - private fun getAllBuildingsForPlayer(playerType: PlayerType, filter: (Building) -> Boolean, y: Int): List { - return buildings - .filter { b -> b.playerType == playerType && b.y == y } - .filter(filter) - } - - private fun canAffordBuilding(buildingType: BuildingType): Boolean { - val cost = when (buildingType) { - BuildingType.DEFENSE -> gameDetails.buildingPrices.getOrDefault(BuildingType.DEFENSE, 100000) - BuildingType.ATTACK -> gameDetails.buildingPrices.getOrDefault(BuildingType.ATTACK, 100000) - BuildingType.ENERGY -> gameDetails.buildingPrices.getOrDefault(BuildingType.ENERGY, 100000) - } - return myself.energy >= cost - } - -} diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt deleted file mode 100644 index a1eb820..0000000 --- a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt +++ /dev/null @@ -1,45 +0,0 @@ -package za.co.entelect.challenge - -import java.util.HashMap - -data class Building( - val x: Int, val y: Int, val playerType: PlayerType, - val health: Int, - val constructionTimeLeft: Int, - val price: Int, - val weaponDamage: Int, - val weaponSpeed: Int, - val weaponCooldownTimeLeft: Int, - val weaponCooldownPeriod: Int, - val destroyMultiplier: Int, - val constructionScore: Int, - val energyGeneratedPerTurn: Int, - val buildingType: BuildingType) - -data class Missile( - val x: Int, val y: Int, val playerType: PlayerType, - val damage: Int, - val speed: Int) - -data class Player( - val playerType: PlayerType, - val energy: Int, - val health: Int, - val hitsTaken: Int, - val score: Int) - -data class CellStateContainer( - val x: Int, val y: Int, val playerType: PlayerType, - val buildings: List, - val missiles: List) - -data class GameDetails( - val round: Int, - val mapWidth: Int, - val mapHeight: Int, - val buildingPrices: HashMap) - -data class GameState( - val players: Array, - val gameMap: Array>, - var gameDetails: GameDetails) diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt deleted file mode 100644 index d9c9d75..0000000 --- a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt +++ /dev/null @@ -1,25 +0,0 @@ -package za.co.entelect.challenge - -enum class BuildingType { - DEFENSE, - ATTACK, - ENERGY -} - -fun isAttackBuilding(building: Building) : Boolean { - return building.buildingType == BuildingType.ATTACK -} - -fun isDefenceBuilding(building: Building) : Boolean { - return building.buildingType == BuildingType.DEFENSE -} - -enum class Direction private constructor(val multiplier: Int) { - LEFT(-1), - RIGHT(1) -} - -enum class PlayerType { - A, - B -} \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt deleted file mode 100644 index 5dbe23f..0000000 --- a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt +++ /dev/null @@ -1,41 +0,0 @@ -package za.co.entelect.challenge - -import com.google.gson.Gson - -import java.io.* - -object Main { - private const val COMMAND_FILE_NAME = "command.txt" - private const val STATE_FILE_NAME = "state.json" - - @JvmStatic - fun main(args: Array) { - var state = "" - try { - val br = BufferedReader(FileReader(STATE_FILE_NAME)) - state = br.readLine() - } catch (e: IOException) { - e.printStackTrace() - } - - val gson = Gson() - val gameState = gson.fromJson(state, GameState::class.java) - - val bot = Bot(gameState) - val command = bot.run() - - writeBotResponseToFile(command) - } - - private fun writeBotResponseToFile(command: String) { - try { - val bufferedWriter = BufferedWriter(FileWriter(File(COMMAND_FILE_NAME))) - bufferedWriter.write(command) - bufferedWriter.flush() - bufferedWriter.close() - } catch (e: IOException) { - e.printStackTrace() - } - - } -} 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/README.md b/starter-pack/starter-bots/python3/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/starter-pack/starter-bots/python3/StarterBot.py b/starter-pack/starter-bots/python3/StarterBot.py deleted file mode 100644 index 4b0e81b..0000000 --- a/starter-pack/starter-bots/python3/StarterBot.py +++ /dev/null @@ -1,265 +0,0 @@ -# -*- coding: utf-8 -*- -''' -Entelect StarterBot for Python3 -''' -import time - -startTime = time.time() - -import json -import os -from time import sleep -import random - - - - -class StarterBot: - - def __init__(self,state_location): - ''' - Initialize Bot. - Load all game state information. - ''' - try: - self.game_state = self.loadState(state_location) - except IOError: - print("Cannot load Game State") - - self.full_map = self.game_state['gameMap'] - self.rows = self.game_state['gameDetails']['mapHeight'] - self.columns = self.game_state['gameDetails']['mapWidth'] - self.command = '' - - self.player_buildings = self.getPlayerBuildings() - self.opponent_buildings = self.getOpponentBuildings() - self.projectiles = self.getProjectiles() - - self.player_info = self.getPlayerInfo('A') - self.opponent_info = self.getPlayerInfo('B') - - 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']} - return None - - - def loadState(self,state_location): - ''' - Gets the current Game State json file. - ''' - return json.load(open(state_location,'r')) - - def getPlayerInfo(self,playerType): - ''' - Gets the player information of specified player type - ''' - for i in range(len(self.game_state['players'])): - if self.game_state['players'][i]['playerType'] == playerType: - return self.game_state['players'][i] - else: - continue - return None - - def getOpponentBuildings(self): - ''' - Looks for all buildings, regardless if completed or not. - 0 - Nothing - 1 - Attack Unit - 2 - Defense Unit - 3 - Energy Unit - ''' - opponent_buildings = [] - - for row in range(0,self.rows): - buildings = [] - for col in range(int(self.columns/2),self.columns): - if (len(self.full_map[row][col]['buildings']) == 0): - buildings.append(0) - elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ATTACK'): - buildings.append(1) - elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'DEFENSE'): - buildings.append(2) - elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ENERGY'): - buildings.append(3) - else: - buildings.append(0) - - opponent_buildings.append(buildings) - - return opponent_buildings - - def getPlayerBuildings(self): - ''' - Looks for all buildings, regardless if completed or not. - 0 - Nothing - 1 - Attack Unit - 2 - Defense Unit - 3 - Energy Unit - ''' - player_buildings = [] - - for row in range(0,self.rows): - buildings = [] - for col in range(0,int(self.columns/2)): - if (len(self.full_map[row][col]['buildings']) == 0): - buildings.append(0) - elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ATTACK'): - buildings.append(1) - elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'DEFENSE'): - buildings.append(2) - elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ENERGY'): - buildings.append(3) - else: - buildings.append(0) - - player_buildings.append(buildings) - - return player_buildings - - def getProjectiles(self): - ''' - Find all projectiles on the map. - 0 - Nothing there - 1 - Projectile belongs to player - 2 - Projectile belongs to opponent - ''' - projectiles = [] - - for row in range(0,self.rows): - temp = [] - for col in range(0,self.columns): - if (len(self.full_map[row][col]['missiles']) == 0): - temp.append(0) - elif (self.full_map[row][col]['missiles'][0]['playerType'] == 'A'): - temp.append(1) - elif (self.full_map[row][col]['missiles'][0]['playerType'] == 'B'): - temp.append(2) - - projectiles.append(temp) - - return projectiles - - - def checkDefense(self, lane_number): - - ''' - Checks a lane. - Returns True if lane contains defense unit. - ''' - - lane = list(self.opponent_buildings[lane_number]) - if (lane.count(2) > 0): - return True - else: - return False - - def checkMyDefense(self, lane_number): - - ''' - Checks a lane. - Returns True if lane contains defense unit. - ''' - - lane = list(self.player_buildings[lane_number]) - if (lane.count(2) > 0): - return True - else: - return False - - def checkAttack(self, lane_number): - - ''' - Checks a lane. - Returns True if lane contains attack unit. - ''' - - lane = list(self.opponent_buildings[lane_number]) - if (lane.count(1) > 0): - return True - else: - return False - - def getUnOccupied(self,lane): - ''' - Returns index of all unoccupied cells in a lane - ''' - indexes = [] - for i in range(len(lane)): - if lane[i] == 0 : - indexes.append(i) - - return indexes - - - def generateAction(self): - ''' - Place your bot logic here ! - - - If there is an opponent attack unit on a row, and you have enough energy for a defense - Build a defense at a random unoccupied location on that row if it is undefended. - - Else If you have enough energy for the most expensive building - Build a random building type at a random unoccupied location - - Else: - Save energy until you have enough for the most expensive building - - Building Types : - 0 : Defense Building - 1 : Attack Building - 2 : Energy Building - ''' - lanes = [] - x,y,building = 0,0,0 - #check all lanes for an attack unit - for i in range(self.rows): - 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): - #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 - #A count of 0 would mean all lanes are not under attack - if (len(lanes) > 0) : - #Chose a random lane under attack to place a defensive unit - #Chose a cell that is unoccupied in that lane - building = 0 - y = random.choice(lanes) - 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()): - building = random.choice([0,1,2]) - x = random.randint(0,self.rows) - y = random.randint(0,int(self.columns/2)-1) - else: - self.writeDoNothing() - return None - - self.writeCommand(x,y,building) - return x,y,building - - def writeCommand(self,x,y,building): - ''' - command in form : x,y,building_type - ''' - outfl = open('command.txt','w') - outfl.write(','.join([str(x),str(y),str(building)])) - outfl.close() - return None - - def writeDoNothing(self): - ''' - command in form : x,y,building_type - ''' - outfl = open('command.txt','w') - outfl.write("") - outfl.close() - return None - -if __name__ == '__main__': - s = StarterBot('state.json') - s.generateAction() - \ No newline at end of file diff --git a/starter-pack/starter-bots/python3/bot.json b/starter-pack/starter-bots/python3/bot.json deleted file mode 100644 index fd7b285..0000000 --- a/starter-pack/starter-bots/python3/bot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "author": "John Doe", - "email": "john.doe@example.com", - "nickName": "Guido", - "botLocation": "/", - "botFileName": "StarterBot.py", - "botLanguage": "python3" -} \ No newline at end of file diff --git a/starter-pack/starter-bots/rust/.gitignore b/starter-pack/starter-bots/rust/.gitignore deleted file mode 100644 index 44ba2ac..0000000 --- a/starter-pack/starter-bots/rust/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -target -command.txt -state.json - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk \ No newline at end of file diff --git a/starter-pack/starter-bots/rust/Cargo.toml b/starter-pack/starter-bots/rust/Cargo.toml deleted file mode 100644 index a1ac517..0000000 --- a/starter-pack/starter-bots/rust/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "entelect_challenge_rust_sample" -version = "1.0.0" - -[dependencies] -serde_derive = "1.0.43" -serde = "1.0.43" -serde_json = "1.0.16" - -rand = "0.4.2" diff --git a/starter-pack/starter-bots/rust/README.md b/starter-pack/starter-bots/rust/README.md deleted file mode 100644 index 0b97c14..0000000 --- a/starter-pack/starter-bots/rust/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Rust Sample Bot - -Rust is a systems programming language, giving programmers the low -level control that they would usually associate with a programming -langauge like C or C++, but modern high level programming features. - -Rust is a compiled language, which compiles to an -architecture-specific binary. - -For getting started with this bot in particular, I've done a write up -about [writing a Rust bot for the Entelect challenge](https://www.worthe-it.co.za/programming/2018/05/02/writing-an-entelect-challenge-bot-in-rust.html). - -## Environment Setup - -The Rust compiler toolchain can be downloaded from the Rust project -website. - -https://www.rust-lang.org/en-US/install.html - -## Compilation - -The bot can be built using the Rust build tool, Cargo. For the sake of -the competition, the `--release` flag should be used. - -``` -cargo build --release -``` - -## Running - -After compilation, there will be an executable in -`target/release/`. - -For example, this sample bot's name is -`entelect_challenge_rust_sample`, so the executable to be run is -`target/release/entelect_challenge_rust_sample` on Linux or -`target/release/entelect_challenge_rust_sample.exe` on Windows. - diff --git a/starter-pack/starter-bots/rust/bot.json b/starter-pack/starter-bots/rust/bot.json deleted file mode 100644 index 4aabc28..0000000 --- a/starter-pack/starter-bots/rust/bot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "author": "John Doe", - "email": "john.doe@example.com", - "nickName": "Graydon", - "botLocation": "/target/release/", - "botFileName": "entelect_challenge_rust_sample", - "botLanguage": "rust" -} diff --git a/starter-pack/starter-bots/rust/src/main.rs b/starter-pack/starter-bots/rust/src/main.rs deleted file mode 100644 index 0029419..0000000 --- a/starter-pack/starter-bots/rust/src/main.rs +++ /dev/null @@ -1,168 +0,0 @@ -extern crate serde; -extern crate serde_json; - -#[macro_use] -extern crate serde_derive; - -extern crate rand; -use rand::{thread_rng, Rng}; - -use std::error::Error; - -#[repr(u8)] -#[derive(Debug, Clone, Copy)] -enum Building { - Defense = 0, - Attack = 1, - Energy = 2, -} - -impl std::fmt::Display for Building { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", *self as u8) - } -} - -const STATE_PATH: &str = "state.json"; - -const COMMAND_PATH: &str = "command.txt"; - -#[derive(Debug, Clone, Copy)] -struct Command { - x: u32, - y: u32, - building: Building, -} - -impl std::fmt::Display for Command { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{},{},{}", self.x, self.y, self.building) - } -} - -mod state; - -fn current_energy(state: &state::State) -> u32 { - state.players.iter() - .filter(|p| p.player_type == 'A') - .map(|p| p.energy) - .next() - .unwrap_or(0) -} - -fn can_afford_all_buildings(state: &state::State) -> bool { - can_afford_attack_buildings(state) && - can_afford_defence_buildings(state) && - can_afford_energy_buildings(state) -} - -fn can_afford_attack_buildings(state: &state::State) -> bool { - current_energy(state) >= state.game_details.building_prices.attack -} -fn can_afford_defence_buildings(state: &state::State) -> bool { - current_energy(state) >= state.game_details.building_prices.defense -} -fn can_afford_energy_buildings(state: &state::State) -> bool { - current_energy(state) >= state.game_details.building_prices.energy -} - -fn is_under_attack(state: &state::State, y: u32) -> bool { - let attack = state.game_map[y as usize].iter() - .any(|cell| cell.buildings.iter() - .any(|building| building.player_type == 'B' && - building.building_type == "ATTACK")); - let defences = state.game_map[y as usize].iter() - .any(|cell| cell.buildings.iter() - .any(|building| building.player_type == 'A' && - building.building_type == "DEFENSE")); - attack && !defences -} - -fn is_occupied(state: &state::State, x: u32, y: u32) -> bool { - !state.game_map[y as usize][x as usize].buildings.is_empty() -} - -fn unoccupied_in_row(state: &state::State, y: u32) -> Vec { - (0..state.game_details.map_width/2) - .filter(|&x| !is_occupied(&state, x, y)) - .collect() -} - -fn unoccupied_cells(state: &state::State) -> Vec<(u32, u32)> { - (0..state.game_details.map_width/2) - .flat_map(|x| (0..state.game_details.map_height) - .map(|y| (x, y)) - .collect::>()) - .filter(|&(x, y)| !is_occupied(&state, x, y)) - .collect() -} - -fn choose_move(state: &state::State) -> Option { - let mut rng = thread_rng(); - - if can_afford_defence_buildings(state) { - for y in 0..state.game_details.map_height { - if is_under_attack(state, y) { - let x_options = unoccupied_in_row(state, y); - if let Some(&x) = rng.choose(&x_options) { - return Some(Command { - x: x, - y: y, - building: Building::Defense - }); - } - } - } - } - - if can_afford_all_buildings(state) { - let options = unoccupied_cells(state); - let option = rng.choose(&options); - let buildings = [Building::Attack, Building::Defense, Building::Energy]; - let building = rng.choose(&buildings); - match (option, building) { - (Some(&(x, y)), Some(&building)) => Some(Command { - x: x, - y: y, - building: building - }), - _ => None - } - } - else { - None - } -} - -use std::fs::File; -use std::io::prelude::*; - -fn write_command(filename: &str, command: Option) -> Result<(), Box > { - let mut file = File::create(filename)?; - if let Some(command) = command { - write!(file, "{}", command)?; - } - - Ok(()) -} - -use std::process; - -fn main() { - let state = match state::read_state_from_file(STATE_PATH) { - Ok(state) => state, - Err(error) => { - eprintln!("Failed to read the {} file. {}", STATE_PATH, error); - process::exit(1); - } - }; - let command = choose_move(&state); - - match write_command(COMMAND_PATH, command) { - Ok(()) => {} - Err(error) => { - eprintln!("Failed to write the {} file. {}", COMMAND_PATH, error); - process::exit(1); - } - } -} diff --git a/starter-pack/starter-bots/rust/src/state.rs b/starter-pack/starter-bots/rust/src/state.rs deleted file mode 100644 index 429db6d..0000000 --- a/starter-pack/starter-bots/rust/src/state.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::fs::File; -use std::io::prelude::*; -use serde_json; -use std::error::Error; - -pub fn read_state_from_file(filename: &str) -> Result> { - let mut file = File::open(filename)?; - let mut content = String::new(); - file.read_to_string(&mut content)?; - let state = serde_json::from_str(content.as_ref())?; - Ok(state) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct State { - pub game_details: GameDetails, - pub players: Vec, - pub game_map: Vec>, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GameDetails { - pub round: u32, - pub map_width: u32, - pub map_height: u32, - pub building_prices: BuildingPrices -} - -#[derive(Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub struct BuildingPrices { - pub energy: u32, - pub defense: u32, - pub attack: u32 -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Player { - pub player_type: char, - pub energy: u32, - pub health: u32, - pub hits_taken: u32, - pub score: u32 -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GameCell { - pub x: u32, - pub y: u32, - pub buildings: Vec, - pub missiles: Vec, - pub cell_owner: char -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildingState { - pub health: u32, - pub construction_time_left: i32, - pub price: u32, - pub weapon_damage: u32, - pub weapon_speed: u32, - pub weapon_cooldown_time_left: u32, - pub weapon_cooldown_period: u32, - pub destroy_multiplier: u32, - pub construction_score: u32, - pub energy_generated_per_turn: u32, - pub building_type: String, - pub x: u32, - pub y: u32, - pub player_type: char -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MissileState { - pub damage: u32, - pub speed: u32, - pub x: u32, - pub y: u32, - pub player_type: char -} 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 2f2ce678e93701ed08b4f4937279c0bf1a411952..6dbeb10af06554ea453345a4996c9e9151197e17 100644 GIT binary patch delta 114667 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

{AwjgPt11f{xIDK|dZ3 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

^2%k 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�W9*^Sj zR9i!R)Aqu^`uG=uloy9}4dxG7cJSn{wE|ollz?M;c5;wZ`-Vdb%@f?{+&}TW3r2pM zz>vEi6vVqf#y1he#VUqttD*o$xixv&%gItzBax_dl_^KB&l^%J*`^Wg+^3$TrGzJ{xS*lP=qn+(A|cn5-j7Sb;8>#@-QIA6C9AUa9B$fy9&0aqyUVUd{xsfQo-Acf5h7f` zh*sZ(d^}KD>T>I$=&6ip9iT?z>57LuT3SArA+-%qmM5zCuk!^25$r&>Jmp|~HMaGV zKbJZ#7l>_MU>`~Mz2qU%zSSI7cn!)jz2z{ee2pN&dJ(SN^M?9w&}1+TyuA)?2B*iZ zQ$Wg-FjM8muAT7-eQXMm0OT|dn_Hb`UD04`(^>j`viT7_g0Zu&>9y0`w1j2 z!Y4+?aPL`yJq_N0^*$`cV7pdxG@~~A7|x$6n}hZ00YMn#gT#vyB2nanmT3b%J~9!5 ze@L(*n-(bO73T%y(-z2A3cf2c^!F{$^h!Uo1Q&_za_th0sj@|)2}QS&Jt(UsGSKIy zpsw{!w5E!da-8&_N6r-?+ z*2vG^S^-Ax)1+5_i9}s%IRe}CBYfpdY3Dlu%YF~R1J~<@Xn*>`FkCZKuenPbIZ|>r zrT{;=9qnv`zP$lzzMt@;1N~wK$;~56#eNgG7L{sux!dhpxFc~w&AQTQwR*=q^0>`)qBPTf++b{{dDd?61 z&8Xox7n&o$7lL8s$vl>$K<8QBG*yB!xe{u!dY3xb8Wm_e6DqcOzSgxf>eCf7}8CiDkWej!~DecEbk_pl6AP8 zBDK4w2U}8pIK12Rx*lRdFsKPxMj+g7-c^91G)c;OsDOn0ismdizrYvc9W+QtGG>aLe)OgXWzZJ4)0(&JckV1t_rNNOCWTmSfo1l)qb zI$PpB*IKQN!_H(BFHe(JwH0*Pi7==<9;3*d4hq4p36Rw*SSwhKTwaP!a~1VzeW-yz z%&-&WiPCprdVx_=BOD!N*{x1QCHW*$FD6c!(?cZSS{$WOBSX7l1!Qs@yi%Y2{z=IG z-UNZ;Zrhe5*G>q2pWzncFTCmDVuw_s$*uvYIbv?n#d1zp#^#Z-v z1nK4?J+}qD*PsV~w?shNm#QtvzAn{X6nl;gi=N`yf9#D+Y#_a=G-+;;Nc`9b zo*Puy7jwn2xFLa=0yABONZ#BW>&PMaBEP3;mGCCVK?Lt3MjWvHKNhY(UUXeoOmZ1%dTAEwI{)f@?4B4E9eFs+DVW_pJ*FX@iJS_;e4Mg!2>40NNPy#7vUtnWuE8LauD#E%YwJ$GgvifkQ``) z!=Z!ZvC_tC0%1J{Gd2Ig7&*MZDIlu`Bj#7$7Ky%hP($iv`eZ<6%{_|Dkh7&_PZ$l) zQnEAxH^(uH+r2>6>KUfJGzgJr7|~|PZE62YH9pb~MVyKc*NjP0R@K{eg2;L(+T&%|F+UXSGmtQ9MHNFaFg;}^V7td9VF*7Ab>pI?fRy(I zRwgEj$xw;Ed2fok%RQk3wjE-chi134(dNSa{2`XcUyp?<8=SM#0yrFp)SH z2135h(tk8)CUq6y($NTtmy6&;v$urKbS58VkQRmqp^4Y)~Rc$Aa)F_Wd({_!WodSroMy z2k^lD0{qEPy&r9Z|o0!)py zAMj1s3a9p~M$ULh=rBzX92}3rzB-#;j+e(v1q%d_w^I2N&`PB(5)kXf*ufya!my{AIlfi;3IynnAU0_q~rliKSd@{niaJv@FzPMxS6`pHivz31t z%jPnH%h-u9NSFfZ?{*3ZmI0k11*rLSP`4*+= z@8MCbSmM<9M?KhzΞO5O%lNP|ghIFo|X_!AvNl8J4m9XCnB^9kIX3dKP}2n}Jp} zxRD`2jC8=n5Q0^hnW)&Kto{quNeY&pIsAWcxOaUf>jb=#mBZIu+A~WYBVG1Za75%` zUS8#+fCx+TVBd~b{{?5aPIgz+!}!_A=5=2s)m#jTSIm|pjWD>IjX9ed4{7P00A=>% z8I)`+%$qw0?0L9~LQmkYWw-rr?b;rhtn8ec;*Mh_Q=Bvq*BUcOL>LaY#BIYj4NOwa z>DXM^OZqLAjOL@X`6fA=d1$*@$1zAKgP7CAd7zoeAzj(7y2f;mL3Z_GG~=aN8YbD5 zmd{6dAK-}=RLv4^kjTeEL;D42-44%}-RQZD0-nA=ZY7PE1y{ua%rZXhtxs5z&q7oO z-+p?C1O!)gx&t^Up(0?llx2Oi7|N5vSqNwzlce3rCZC; zi>)eVehQ?NeB|?=jVv)rx?3uM9?Q|Vox&Z!jAFrZG+suy`Iou1dN~#fw`>)OgcS(L ziMaKbVLFs0JJY-6i13>$keTQE1bFBEWLE~Zq;3T$I#;nOOVa<)da# zT|2(e623^bqJq`3uj(^wMZi71ZK&NV#8}O0xs__K4wU;U*^o zr2($AaT7K^mH{Ui+2gY+OB^ZV$hu8vE8ha?&CEA*y*~OVUjF=mr$++~9`%d|1J9C5 zN^zooYqbn*Qd@0zw`=lhvrS44c==e%{RFE&C=eJ%uMl$KWCL4hF){J8;@| zC$I z8HXXz8&B053OqvIMRKAS)B0$cl~)rGy@F7~H%-suieFH1wa=fQ^z{Rk8*B?T$VX^P zuGYsmHY;<;xDk|ABsb+Om202m9s)~KXg7Rb_E>AdViGrG;+W4eGdT}0#BS?ly!GCe zD%PXzOKFv2O&LY9CHKx>CD$4@g}A!^$4*>{vq5f2hka9AI62q;+TN=tBmW2Fh6e8r z5ZbAoUkbWWo&3eErhnxGhNoaKd;k`g92BGNjZ3Z1+q?nUz1%?j2E>MWTNaFSiyc$i z(D4nj8_(074T$sxp<1wWV2Vw>l_&2;P1_GScJPtm3)z!8Yptv)mOZ%~+l`PjqnozI zpk{>fj2gRtQ)rIx{T_b_1|{<>yq*>pHiV@(^YI!NR>y>;M=A+YOJsMB%Ps-#Om{7g z*RrgxDUMWJtgUKQjG#8To}dL)?ZBh(2q#J?Wmal9?i2D%NwK93rKmV8)J{xX@~Q?N ztHN8%hR-G!^#Ma_pcg$YmEAe*p`RT+C*e(BOPBvX27F_HvW|a{5hUz&d(KtzRt|fB v5Zwn(!|JwnL_JpvPV3OK!0`xt9|PYTK2Ll;v%aD5HXk2i+@LZm1+M)+@7o$5 delta 114142 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 Date: Fri, 11 May 2018 21:19:00 +0200 Subject: [PATCH 17/20] Update the ReadMe & example state.json to include new bots & properties --- starter-pack/ReadMe.txt | 31 +++- starter-pack/examples/example-state.json | 223 +++++++++++++---------- 2 files changed, 153 insertions(+), 101 deletions(-) diff --git a/starter-pack/ReadMe.txt b/starter-pack/ReadMe.txt index 5dcdae6..9858291 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,8 @@ 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. + "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 +83,9 @@ Javascript => "javascript" Rust => "rust" C++ => "c++" Kotlin => "kotlin" +Golang => "golang" +Haskell => "haskell" +PHP => "php" @@ -141,3 +146,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/kotlin] + +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/kotlin] + +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/kotlin] 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 From d03321b66968ca91e881a1bf2437137f8a21bfaf Mon Sep 17 00:00:00 2001 From: Pierre Roux Date: Fri, 11 May 2018 21:19:41 +0200 Subject: [PATCH 18/20] Add new starter bots to starter-pack --- .../starter-bots/cplusplus/Readme.txt | 24 ++ starter-pack/starter-bots/cplusplus/bot.json | 8 + .../starter-bots/cplusplus/compile.bat | 1 + .../starter-bots/cplusplus/samplebot.cpp | 314 ++++++++++++++++++ .../starter-bots/cplusplus/samplebot.exe | Bin 0 -> 173751 bytes .../starter-bots/csharpcore/.gitignore | 33 ++ .../starter-bots/csharpcore/StarterBot.sln | 25 ++ .../starter-bots/csharpcore/StarterBot/Bot.cs | 156 +++++++++ .../StarterBot/Entities/Building.cs | 18 + .../StarterBot/Entities/BuildingStats.cs | 25 ++ .../csharpcore/StarterBot/Entities/Cell.cs | 12 + .../StarterBot/Entities/CellStateContainer.cs | 14 + .../StarterBot/Entities/GameDetails.cs | 14 + .../StarterBot/Entities/GameState.cs | 11 + .../csharpcore/StarterBot/Entities/Missile.cs | 8 + .../csharpcore/StarterBot/Entities/Player.cs | 13 + .../StarterBot/Enums/BuildingType.cs | 18 + .../csharpcore/StarterBot/Enums/Direction.cs | 8 + .../csharpcore/StarterBot/Enums/PlayerType.cs | 8 + .../csharpcore/StarterBot/Program.cs | 22 ++ .../csharpcore/StarterBot/StarterBot.csproj | 12 + .../csharpcore/StarterBot/state.json | 1 + starter-pack/starter-bots/csharpcore/bot.json | 8 + starter-pack/starter-bots/golang/README.md | 17 + .../starter-bots/golang/starterbot.go | 233 +++++++++++++ starter-pack/starter-bots/haskell/.gitignore | 15 + .../starter-bots/haskell/ChangeLog.md | 3 + starter-pack/starter-bots/haskell/LICENSE | 14 + starter-pack/starter-bots/haskell/README.md | 26 ++ starter-pack/starter-bots/haskell/Setup.hs | 2 + starter-pack/starter-bots/haskell/src/Bot.hs | 122 +++++++ .../starter-bots/haskell/src/Interpretor.hs | 223 +++++++++++++ starter-pack/starter-bots/haskell/stack.yaml | 66 ++++ .../starter-bots/haskell/test/Spec.hs | 2 + starter-pack/starter-bots/java/.gitignore | 54 +++ starter-pack/starter-bots/java/bot.json | 8 + starter-pack/starter-bots/java/pom.xml | 55 +++ .../java/za/co/entelect/challenge/Bot.java | 191 +++++++++++ .../java/za/co/entelect/challenge/Main.java | 46 +++ .../entelect/challenge/entities/Building.java | 18 + .../challenge/entities/BuildingStats.java | 15 + .../co/entelect/challenge/entities/Cell.java | 36 ++ .../entities/CellStateContainer.java | 31 ++ .../challenge/entities/GameDetails.java | 14 + .../challenge/entities/GameState.java | 29 ++ .../entelect/challenge/entities/Missile.java | 6 + .../entelect/challenge/entities/Player.java | 11 + .../challenge/enums/BuildingType.java | 22 ++ .../entelect/challenge/enums/Direction.java | 16 + .../entelect/challenge/enums/PlayerType.java | 6 + .../starter-bots/javascript/StarterBot.js | 174 ++++++++++ starter-pack/starter-bots/javascript/bot.json | 8 + starter-pack/starter-bots/kotlin/.gitignore | 54 +++ starter-pack/starter-bots/kotlin/bot.json | 8 + starter-pack/starter-bots/kotlin/pom.xml | 129 +++++++ .../kotlin/za/co/entelect/challenge/Bot.kt | 129 +++++++ .../za/co/entelect/challenge/Entities.kt | 45 +++ .../kotlin/za/co/entelect/challenge/Enums.kt | 25 ++ .../kotlin/za/co/entelect/challenge/Main.kt | 41 +++ starter-pack/starter-bots/php/README.md | 14 + starter-pack/starter-bots/php/StarterBot.php | 16 + starter-pack/starter-bots/python3/README.md | 0 .../starter-bots/python3/StarterBot.py | 289 ++++++++++++++++ starter-pack/starter-bots/python3/bot.json | 8 + starter-pack/starter-bots/rust/.gitignore | 10 + starter-pack/starter-bots/rust/Cargo.toml | 10 + starter-pack/starter-bots/rust/README.md | 38 +++ starter-pack/starter-bots/rust/bot.json | 8 + starter-pack/starter-bots/rust/src/main.rs | 168 ++++++++++ starter-pack/starter-bots/rust/src/state.rs | 86 +++++ 70 files changed, 3294 insertions(+) create mode 100644 starter-pack/starter-bots/cplusplus/Readme.txt create mode 100644 starter-pack/starter-bots/cplusplus/bot.json create mode 100644 starter-pack/starter-bots/cplusplus/compile.bat create mode 100644 starter-pack/starter-bots/cplusplus/samplebot.cpp create mode 100644 starter-pack/starter-bots/cplusplus/samplebot.exe create mode 100644 starter-pack/starter-bots/csharpcore/.gitignore create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot.sln create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/BuildingStats.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Cell.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameState.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/Program.cs create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj create mode 100644 starter-pack/starter-bots/csharpcore/StarterBot/state.json create mode 100644 starter-pack/starter-bots/csharpcore/bot.json create mode 100644 starter-pack/starter-bots/golang/README.md create mode 100644 starter-pack/starter-bots/golang/starterbot.go create mode 100644 starter-pack/starter-bots/haskell/.gitignore create mode 100644 starter-pack/starter-bots/haskell/ChangeLog.md create mode 100644 starter-pack/starter-bots/haskell/LICENSE create mode 100644 starter-pack/starter-bots/haskell/README.md create mode 100644 starter-pack/starter-bots/haskell/Setup.hs create mode 100644 starter-pack/starter-bots/haskell/src/Bot.hs create mode 100644 starter-pack/starter-bots/haskell/src/Interpretor.hs create mode 100644 starter-pack/starter-bots/haskell/stack.yaml create mode 100644 starter-pack/starter-bots/haskell/test/Spec.hs create mode 100644 starter-pack/starter-bots/java/.gitignore create mode 100644 starter-pack/starter-bots/java/bot.json create mode 100644 starter-pack/starter-bots/java/pom.xml create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/BuildingStats.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Cell.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java create mode 100644 starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java create mode 100644 starter-pack/starter-bots/javascript/StarterBot.js create mode 100644 starter-pack/starter-bots/javascript/bot.json create mode 100644 starter-pack/starter-bots/kotlin/.gitignore create mode 100644 starter-pack/starter-bots/kotlin/bot.json create mode 100644 starter-pack/starter-bots/kotlin/pom.xml create mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt create mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt create mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt create mode 100644 starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt create mode 100644 starter-pack/starter-bots/php/README.md create mode 100644 starter-pack/starter-bots/php/StarterBot.php create mode 100644 starter-pack/starter-bots/python3/README.md create mode 100644 starter-pack/starter-bots/python3/StarterBot.py create mode 100644 starter-pack/starter-bots/python3/bot.json create mode 100644 starter-pack/starter-bots/rust/.gitignore create mode 100644 starter-pack/starter-bots/rust/Cargo.toml create mode 100644 starter-pack/starter-bots/rust/README.md create mode 100644 starter-pack/starter-bots/rust/bot.json create mode 100644 starter-pack/starter-bots/rust/src/main.rs create mode 100644 starter-pack/starter-bots/rust/src/state.rs diff --git a/starter-pack/starter-bots/cplusplus/Readme.txt b/starter-pack/starter-bots/cplusplus/Readme.txt new file mode 100644 index 0000000..81ef64c --- /dev/null +++ b/starter-pack/starter-bots/cplusplus/Readme.txt @@ -0,0 +1,24 @@ +Requires MinGW +https://nuwen.net/files/mingw/mingw-15.4.exe is a nicely maintained distro with a few other tools but there are others. + +Extracted the above files to C:\ + +compile.bat needs to be updated to your local MinGW path + +C++ Source code: samplebot.cpp +Compile with : compile.bat +Run bot : samplebot.exe + +I'm using the TextMap.txt, haven't used json before. + +Assumptions + - No inputs are given to the bot at runtime + - The working directory contains TextMap.txt + - PlayerCommand.txt will be written to the working directory + +Still to do + - Remove random moves and add simple logic + - Read in from json file + +bot.json file + - might need to have values for compile and running diff --git a/starter-pack/starter-bots/cplusplus/bot.json b/starter-pack/starter-bots/cplusplus/bot.json new file mode 100644 index 0000000..c98b6f3 --- /dev/null +++ b/starter-pack/starter-bots/cplusplus/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "Bjarne", + "botLocation": "/", + "botFileName": "samplebot", + "botLanguage": "c++" +} \ No newline at end of file diff --git a/starter-pack/starter-bots/cplusplus/compile.bat b/starter-pack/starter-bots/cplusplus/compile.bat new file mode 100644 index 0000000..2b76842 --- /dev/null +++ b/starter-pack/starter-bots/cplusplus/compile.bat @@ -0,0 +1 @@ +C:\MinGW\bin\g++.exe -Wall samplebot.cpp -o samplebot.exe \ No newline at end of file diff --git a/starter-pack/starter-bots/cplusplus/samplebot.cpp b/starter-pack/starter-bots/cplusplus/samplebot.cpp new file mode 100644 index 0000000..68a9383 --- /dev/null +++ b/starter-pack/starter-bots/cplusplus/samplebot.cpp @@ -0,0 +1,314 @@ +#include //not needed but useful for debugging +#include +#include +#include //for random number generation +#include //seeding random number generation +using namespace std; + +//maximum size for game state - Unknown +const int MAX_MAP_WIDTH = 100; +const int MAX_MAP_HEIGHT = 100; + +class BuildingInfo +{ + public: + string type; + int energyCost; + + //other possible info + int constructionTime; + int health; + int weaponCooldownTime; + int weaponDamage; + int energyGenerated; + + BuildingInfo(){} + + BuildingInfo(string in_type,int in_energyCost) + { + type = in_type; + energyCost = in_energyCost; + } +}; + +class Building +{ + public: + char buildingType; + char owner; + int constructionTimeLeft; + int health; + int weaponCooldownTimeLeft; + int weaponDamage; + int energyGeneratedPerTurn; + + Building() + { + buildingType = 'N'; + } +}; + +class Missile +{ + public: + char owner; + int damage; + + Missile() + {} + + Missile(char in_owner,int in_damage) + { + owner = in_owner; + damage = in_damage; + } + +}; + +class Player +{ + public: + char owner; + int energy; + int health; + int hitsTaken; + int score; + + Player() + {} +}; + +class Cell +{ + public: + char owner; + Building building; + vector missile; + + Cell(){} +}; + +class Gamestate +{ + public: + int roundNumber; + int mapWidth; + int mapHeight; + + vector buildingInfo; + + Player player[2]; + + Cell cell[MAX_MAP_WIDTH][MAX_MAP_HEIGHT]; + + //unknown values + int baseEnergyGenerated; + int maxRounds; + + Gamestate() + { + //setting guessed values + baseEnergyGenerated = 5; + maxRounds = 1000; + } + + //reads gamestate from TextMap.txt + void readinText() + { + string inputline; + ifstream statefile ("textMap.txt"); + + while ( getline(statefile,inputline) ) + { + //start of game info input + if(inputline == "XXXXXXXXX GAME INFO XXXXXXXXX") + { + getline(statefile,inputline); + roundNumber = stoi(inputline.substr(15)); + + getline(statefile,inputline); + mapWidth = stoi(inputline.substr(12)); + + getline(statefile,inputline); + mapHeight = stoi(inputline.substr(13)); + + getline(statefile,inputline); + } + + //start of building info input + if(inputline == "****** BUILDING PRICES ******") + { + getline(statefile,inputline); + + while(inputline != "*****************************") + { + string building_type = inputline.substr(0,inputline.find(":")-1); + int building_energyCost = stoi(inputline.substr(inputline.find(":")+2)); + + buildingInfo.push_back(BuildingInfo(building_type,building_energyCost)); + + getline(statefile,inputline); + } + } + + //start of player A inputs + if(inputline == "---------- PLAYER A ----------") + { + player[0].owner = 'A'; + + getline(statefile,inputline); + player[0].energy = stoi(inputline.substr(9)); + + getline(statefile,inputline); + player[0].health = stoi(inputline.substr(9)); + + getline(statefile,inputline); + player[0].hitsTaken = stoi(inputline.substr(12)); + + getline(statefile,inputline); + player[0].score = stoi(inputline.substr(8)); + + getline(statefile,inputline); + } + + //start of player B inputs + if(inputline == "---------- PLAYER B ----------") + { + player[1].owner = 'B'; + + getline(statefile,inputline); + player[1].energy = stoi(inputline.substr(9)); + + getline(statefile,inputline); + player[1].health = stoi(inputline.substr(9)); + + getline(statefile,inputline); + player[1].hitsTaken = stoi(inputline.substr(12)); + + getline(statefile,inputline); + player[1].score = stoi(inputline.substr(8)); + + getline(statefile,inputline); + } + + //start of map input + if(inputline == "############# MAP #############") + { + for(int map_y = 0;map_y < mapHeight;map_y++) + { + getline(statefile,inputline); + for(int map_x = 0;map_x < mapWidth;map_x++) + { + cell[map_x][map_y].building.buildingType = inputline[map_x*11 + 5]; + cell[map_x][map_y].owner = (map_x * 2 < mapWidth ? 'A' : 'B'); + } + } + } + + //start of building data + if(inputline == "######## BUILDING DATA #########") + { + getline(statefile,inputline);//FORMAT... + getline(statefile,inputline);//blank line + getline(statefile,inputline);//first building info + while(inputline != "###############################") + { + inputline = inputline.substr(inputline.find("[")+1); + int map_x = stoi(inputline.substr(0,inputline.find(","))); + inputline = inputline.substr(inputline.find(",")+1); + int map_y = stoi(inputline.substr(0,inputline.find("]"))); + inputline = inputline.substr(inputline.find(" ")+1); + + cell[map_x][map_y].building.owner = inputline[0]; + inputline = inputline.substr(inputline.find("|")+1); + + cell[map_x][map_y].building.constructionTimeLeft = stoi(inputline.substr(0,inputline.find("|"))); + inputline = inputline.substr(inputline.find("|")+1); + + cell[map_x][map_y].building.health = stoi(inputline.substr(0,inputline.find("|"))); + inputline = inputline.substr(inputline.find("|")+1); + + cell[map_x][map_y].building.weaponCooldownTimeLeft = stoi(inputline.substr(0,inputline.find("|"))); + inputline = inputline.substr(inputline.find("|")+1); + + cell[map_x][map_y].building.weaponDamage = stoi(inputline.substr(0,inputline.find("|"))); + inputline = inputline.substr(inputline.find("|")+1); + + cell[map_x][map_y].building.energyGeneratedPerTurn = stoi(inputline); + + getline(statefile,inputline); + } + } + + //start of missile data + if(inputline == "####### MISSILE DATA ########") + { + getline(statefile,inputline);//FORMAT... + getline(statefile,inputline);//blank line + getline(statefile,inputline);//first missile info + while(inputline != "###############################") + { + inputline = inputline.substr(inputline.find("[")+1); + int map_x = stoi(inputline.substr(0,inputline.find(","))); + inputline = inputline.substr(inputline.find(",")+1); + int map_y = stoi(inputline.substr(0,inputline.find("]"))); + inputline = inputline.substr(inputline.find(" ")+1); + + char missile_owner = inputline[0]; + inputline = inputline.substr(inputline.find("|")+1); + int missile_damage = stoi(inputline); + + cell[map_x][map_y].missile.push_back(Missile(missile_owner,missile_damage)); + + getline(statefile,inputline); + } + } + } + + statefile.close(); + } + + vector possible_moves() + { + vector moves; + moves.push_back(""); + + for(int x = 0;x= buildingInfo[building_type].energyCost) + { + moves.push_back(to_string(x)+","+to_string(y)+","+to_string(building_type)); + } + } + } + } + + return moves; + } +}; + +int main() +{ + //initialize random seed + srand (time(NULL)); + + //create gamestate object + Gamestate gamestate; + gamestate.readinText(); + + //get legal moves rom gamestate + vector possible_moves = gamestate.possible_moves(); + + //write random move to command file + ofstream movefile ("command.txt"); + movefile << possible_moves[rand()%possible_moves.size()]; + movefile.close(); + + return 0; +} \ 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 0000000000000000000000000000000000000000..70522e30f68e6957124dd275703e711073c87955 GIT binary patch literal 173751 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�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 literal 0 HcmV?d00001 diff --git a/starter-pack/starter-bots/csharpcore/.gitignore b/starter-pack/starter-bots/csharpcore/.gitignore new file mode 100644 index 0000000..1174d4b --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/.gitignore @@ -0,0 +1,33 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2017 +.vs/ \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot.sln b/starter-pack/starter-bots/csharpcore/StarterBot.sln new file mode 100644 index 0000000..70410ed --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27428.2005 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StarterBot", "StarterBot\StarterBot.csproj", "{5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E5C2980-94BF-40D7-9F09-7B8B68A63CD9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {88D3E82E-F22C-4F42-A571-DEA3EB1D830E} + EndGlobalSection +EndGlobal diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs new file mode 100644 index 0000000..deb7073 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Bot.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarterBot.Entities; +using StarterBot.Enums; + +namespace StarterBot +{ + public class Bot + { + private readonly GameState _gameState; + + private readonly BuildingStats _attackStats; + private readonly BuildingStats _defenseStats; + private readonly BuildingStats _energyStats; + + private readonly int _mapWidth; + private readonly int _mapHeight; + private readonly Player _player; + private readonly Random _random; + + public Bot(GameState gameState) + { + this._gameState = gameState; + this._mapHeight = gameState.GameDetails.MapHeight; + this._mapWidth = gameState.GameDetails.MapWidth; + + 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); + } + + public string Run() + { + var commandToReturn = ""; + + //This will check if there is enough energy to build any building before processing any commands + if (_player.Energy < _defenseStats.Price || _player.Energy < _energyStats.Price || _player.Energy < _attackStats.Price) + { + return commandToReturn; + } + + //Get all opponent attack buildings + var opponentAttackBuildings = GetBuildings(PlayerType.B, BuildingType.Attack); + + //Get all my attack buildings + var myAttackBuildings = GetBuildings(PlayerType.A, BuildingType.Attack); + //Get all my defense buildings + var myDefenseBuildings = GetBuildings(PlayerType.A, BuildingType.Defense); + + var myBuildings = new List(); + + //Combine all buildings into a single list + myBuildings.AddRange(myAttackBuildings); + myBuildings.AddRange(myDefenseBuildings); + + //Do a random command if enemy has no attack buildings to defend against + if (!opponentAttackBuildings.Any()) + { + commandToReturn = GetRandomCommand(myBuildings); + } + else + { + //Get all rows with enemy buildings where I don't have a defense building + var rows = GetEnemyBuildingRows(opponentAttackBuildings, myDefenseBuildings); + + //Place defense building randomly in first row from list + if (rows.Count > 0) + { + commandToReturn = GetValidAttackCommand(rows[0], myBuildings); + } + else + { + commandToReturn = GetRandomCommand(myBuildings); + } + } + + return commandToReturn; + } + + //Get a valid attack command for a row + public string GetValidAttackCommand(int yCoordinate, List myBuildings) + { + var xRandom = _random.Next(_mapWidth / 2); + + while (myBuildings.Any(x => x.X == xRandom && x.Y == yCoordinate && x.Buildings.Any())) + { + xRandom = _random.Next(_mapWidth / 2); + } + + return $"{xRandom},{yCoordinate},{(int)BuildingType.Defense}"; + } + + //Get random valid command + public string GetRandomCommand(List myBuildings) + { + //Place building randomly on my half of the map + var xRandom = _random.Next(_mapWidth / 2); + var yRandom = _random.Next(_mapHeight); + var btRandom = _random.Next(Enum.GetNames(typeof(BuildingType)).Length); + + while (myBuildings.Any(x => x.X == xRandom && x.Y == yRandom && x.Buildings.Any())) + { + xRandom = _random.Next(_mapWidth / 2); + yRandom = _random.Next(_mapHeight); + } + + return $"{xRandom},{yRandom},{btRandom}"; + } + + //Get all rows which contain an enemy building where I don't have a defense building + private List GetEnemyBuildingRows(List opponentAttackBuildings, + List myDefenseBuildings) + { + var rows = new List(); + foreach (var enemyAttackBuilding in opponentAttackBuildings) + { + if (rows.Contains(enemyAttackBuilding.Y)) continue; + rows.Add(enemyAttackBuilding.Y); + } + + foreach (var myDefenseBuilding in myDefenseBuildings) + { + if (rows.Contains(myDefenseBuilding.Y)) + { + rows.Remove(myDefenseBuilding.Y); + } + } + + return rows; + } + + //Get buildings of a certain type for a certain player + private List GetBuildings(PlayerType playerType, BuildingType buildingType) + { + var list = new List(); + foreach (var cellStateContainers in _gameState.GameMap) + { + foreach (var cellStateContainer in cellStateContainers) + { + if (cellStateContainer.CellOwner == playerType && + cellStateContainer.Buildings.Any(x => x.BuildingType == buildingType)) + { + list.Add(cellStateContainer); + } + } + } + + return list; + } + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs new file mode 100644 index 0000000..858a286 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Building.cs @@ -0,0 +1,18 @@ +using StarterBot.Enums; + +namespace StarterBot.Entities +{ + public class Building + { + public int Health { get; set; } + public int ConstructionTimeLeft { get; set; } + public int Price { get; set; } + public int WeaponDamage { get; set; } + public int WeaponSpeed { get; set; } + public int WeaponCooldownTimeLeft { get; set; } + public int WeaponCooldownPeriod { get; set; } + public int DestroyScore { get; set; } + public int EnergyGeneratedPerTurn { get; set; } + public BuildingType BuildingType { get; set; } + } +} \ No newline at end of file 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/Cell.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Cell.cs new file mode 100644 index 0000000..e94fcbc --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Cell.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; +using StarterBot.Enums; + +namespace StarterBot.Entities +{ + public class Cell + { + public int X { get; set; } + public int Y { get; set; } + public PlayerType PlayerType { get; set; } + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs new file mode 100644 index 0000000..6fff864 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/CellStateContainer.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using StarterBot.Enums; + +namespace StarterBot.Entities +{ + public class CellStateContainer + { + public int X { get; set; } + public int Y { get; set; } + public PlayerType CellOwner { get; set; } + public List Buildings { get; set; } + public List Missiles { get; set; } + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs new file mode 100644 index 0000000..b8c9169 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameDetails.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using StarterBot.Enums; + +namespace StarterBot.Entities +{ + public class GameDetails + { + public int Round { 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/csharpcore/StarterBot/Entities/GameState.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameState.cs new file mode 100644 index 0000000..1be809c --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/GameState.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace StarterBot.Entities +{ + public class GameState + { + public List Players { get; set; } + public CellStateContainer[][] GameMap { get; set; } + public GameDetails GameDetails { get; set; } + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs new file mode 100644 index 0000000..244891c --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Missile.cs @@ -0,0 +1,8 @@ +namespace StarterBot.Entities +{ + public class Missile + { + public int Damage { get; set; } + public int Speed { get; set; } + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs new file mode 100644 index 0000000..5af0439 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Entities/Player.cs @@ -0,0 +1,13 @@ +using StarterBot.Enums; + +namespace StarterBot.Entities +{ + public class Player + { + public PlayerType PlayerType { get; set; } + public int Energy { get; set; } + public int Health { get; set; } + public int HitsTaken { get; set; } + public int Score { get; set; } + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs new file mode 100644 index 0000000..ea5ae04 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/BuildingType.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace StarterBot.Enums +{ + public enum BuildingType + { + [JsonProperty("DEFENSE")] + [EnumMember(Value = "DEFENSE")] + Defense = 0, + [JsonProperty("ATTACK")] + [EnumMember(Value = "ATTACK")] + Attack = 1, + [JsonProperty("ENERGY")] + [EnumMember(Value = "ENERGY")] + Energy = 2 + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs new file mode 100644 index 0000000..2f0b743 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/Direction.cs @@ -0,0 +1,8 @@ +namespace StarterBot.Enums +{ + public enum Direction + { + Left = -1, + Right = 1, + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs new file mode 100644 index 0000000..1fbff88 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Enums/PlayerType.cs @@ -0,0 +1,8 @@ +namespace StarterBot.Enums +{ + public enum PlayerType + { + A, + B + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/Program.cs b/starter-pack/starter-bots/csharpcore/StarterBot/Program.cs new file mode 100644 index 0000000..730852a --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using StarterBot.Entities; + +namespace StarterBot +{ + public class Program + { + private static string _commandFileName = "command.txt"; + private static string _stateFileName = "state.json"; + + static void Main(string[] args) + { + var gameState = JsonConvert.DeserializeObject(File.ReadAllText(_stateFileName)); + + var command = new Bot(gameState).Run(); + + File.WriteAllText(_commandFileName, command); + } + } +} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj b/starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj new file mode 100644 index 0000000..9c432fa --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/StarterBot.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp2.0 + + + + + + + diff --git a/starter-pack/starter-bots/csharpcore/StarterBot/state.json b/starter-pack/starter-bots/csharpcore/StarterBot/state.json new file mode 100644 index 0000000..a86a636 --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/StarterBot/state.json @@ -0,0 +1 @@ +{"gameDetails":{"round":11,"mapWidth":8,"mapHeight":4,"buildingPrices":{"DEFENSE":30,"ENERGY":20,"ATTACK":30}},"players":[{"playerType":"A","energy":30,"health":100,"hitsTaken":0,"score":51},{"playerType":"B","energy":30,"health":100,"hitsTaken":0,"score":51}],"gameMap":[[{"x":0,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":1,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":0,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":4,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":5,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":0,"buildings":[],"missiles":[],"cellOwner":"B"}],[{"x":0,"y":1,"buildings":[{"health":5,"constructionTimeLeft":-1,"price":30,"weaponDamage":5,"weaponSpeed":1,"weaponCooldownTimeLeft":1,"weaponCooldownPeriod":3,"destroyMultiplier":1,"constructionScore":1,"energyGeneratedPerTurn":0,"buildingType":"ATTACK","x":0,"y":1,"playerType":"A"}],"missiles":[],"cellOwner":"A"},{"x":1,"y":1,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":1,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":1,"buildings":[],"missiles":[{"damage":5,"speed":1,"x":3,"y":1,"playerType":"A"}],"cellOwner":"A"},{"x":4,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":5,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":1,"buildings":[],"missiles":[],"cellOwner":"B"}],[{"x":0,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":1,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":2,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":4,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":5,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":2,"buildings":[],"missiles":[],"cellOwner":"B"}],[{"x":0,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":1,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":2,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":3,"y":3,"buildings":[],"missiles":[],"cellOwner":"A"},{"x":4,"y":3,"buildings":[],"missiles":[{"damage":5,"speed":1,"x":4,"y":3,"playerType":"B"}],"cellOwner":"B"},{"x":5,"y":3,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":6,"y":3,"buildings":[],"missiles":[],"cellOwner":"B"},{"x":7,"y":3,"buildings":[{"health":5,"constructionTimeLeft":-1,"price":30,"weaponDamage":5,"weaponSpeed":1,"weaponCooldownTimeLeft":1,"weaponCooldownPeriod":3,"destroyMultiplier":1,"constructionScore":1,"energyGeneratedPerTurn":0,"buildingType":"ATTACK","x":7,"y":3,"playerType":"B"}],"missiles":[],"cellOwner":"B"}]]} \ No newline at end of file diff --git a/starter-pack/starter-bots/csharpcore/bot.json b/starter-pack/starter-bots/csharpcore/bot.json new file mode 100644 index 0000000..1d044cc --- /dev/null +++ b/starter-pack/starter-bots/csharpcore/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "Bill", + "botLocation": "/StarterBot/bin/Debug/netcoreapp2.0", + "botFileName": "StarterBot.dll", + "botLanguage": "c#core" +} \ 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/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/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/.gitignore b/starter-pack/starter-bots/java/.gitignore new file mode 100644 index 0000000..68d1895 --- /dev/null +++ b/starter-pack/starter-bots/java/.gitignore @@ -0,0 +1,54 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ +cmake-build-release/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +\.idea/ +*.iml + +target/* diff --git a/starter-pack/starter-bots/java/bot.json b/starter-pack/starter-bots/java/bot.json new file mode 100644 index 0000000..072dc30 --- /dev/null +++ b/starter-pack/starter-bots/java/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "James", + "botLocation": "/target", + "botFileName": "java-sample-bot-1.0-SNAPSHOT-jar-with-dependencies.jar", + "botLanguage": "java" +} \ No newline at end of file diff --git a/starter-pack/starter-bots/java/pom.xml b/starter-pack/starter-bots/java/pom.xml new file mode 100644 index 0000000..915ef79 --- /dev/null +++ b/starter-pack/starter-bots/java/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + za.co.entelect.challenge + java-sample-bot + 1.1-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + maven-assembly-plugin + + + + za.co.entelect.challenge.Main + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + com.google.code.gson + gson + 2.8.2 + + + + + 1.8 + + \ No newline at end of file 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 new file mode 100644 index 0000000..465afd6 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Bot.java @@ -0,0 +1,191 @@ +package za.co.entelect.challenge; + +import za.co.entelect.challenge.entities.*; +import za.co.entelect.challenge.enums.BuildingType; +import za.co.entelect.challenge.enums.PlayerType; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static za.co.entelect.challenge.enums.BuildingType.ATTACK; +import static za.co.entelect.challenge.enums.BuildingType.DEFENSE; + +public class Bot { + private static final String NOTHING_COMMAND = ""; + private GameState gameState; + private GameDetails gameDetails; + private int gameWidth; + private int gameHeight; + private Player myself; + private Player opponent; + private List buildings; + private List missiles; + + /** + * Constructor + * + * @param gameState the game state + **/ + public Bot(GameState gameState) { + this.gameState = gameState; + gameDetails = gameState.getGameDetails(); + gameWidth = gameDetails.mapWidth; + gameHeight = gameDetails.mapHeight; + myself = gameState.getPlayers().stream().filter(p -> p.playerType == PlayerType.A).findFirst().get(); + opponent = gameState.getPlayers().stream().filter(p -> p.playerType == PlayerType.B).findFirst().get(); + + buildings = gameState.getGameMap().stream() + .flatMap(c -> c.getBuildings().stream()) + .collect(Collectors.toList()); + + missiles = gameState.getGameMap().stream() + .flatMap(c -> c.getMissiles().stream()) + .collect(Collectors.toList()); + } + + /** + * Run + * + * @return the result + **/ + public String run() { + if (isUnderAttack()) { + return defendRow(); + } else if (hasEnoughEnergyForMostExpensiveBuilding()) { + return buildRandom(); + } else { + return doNothingCommand(); + } + } + + /** + * Build random building + * + * @return the result + **/ + private String buildRandom() { + List emptyCells = gameState.getGameMap().stream() + .filter(c -> c.getBuildings().size() == 0 && c.x < (gameWidth / 2)) + .collect(Collectors.toList()); + + if (emptyCells.isEmpty()) { + return doNothingCommand(); + } + + CellStateContainer randomEmptyCell = getRandomElementOfList(emptyCells); + BuildingType randomBuildingType = getRandomElementOfList(Arrays.asList(BuildingType.values())); + + if (!canAffordBuilding(randomBuildingType)) { + return doNothingCommand(); + } + + return randomBuildingType.buildCommand(randomEmptyCell.x, randomEmptyCell.y); + } + + /** + * Has enough energy for most expensive building + * + * @return the result + **/ + private boolean hasEnoughEnergyForMostExpensiveBuilding() { + return gameDetails.buildingsStats.values().stream() + .filter(b -> b.price <= myself.energy) + .toArray() + .length == 3; + } + + /** + * Defend row + * + * @return the result + **/ + private String defendRow() { + for (int i = 0; i < gameHeight; i++) { + boolean opponentAttacking = getAnyBuildingsForPlayer(PlayerType.B, b -> b.buildingType == ATTACK, i); + if (opponentAttacking && canAffordBuilding(DEFENSE)) { + return placeBuildingInRow(DEFENSE, i); + } + } + + return buildRandom(); + } + + /** + * Checks if this is under attack + * + * @return true if this is under attack + **/ + private boolean isUnderAttack() { + //if enemy has an attack building and i dont have a blocking wall + for (int i = 0; i < gameHeight; i++) { + boolean opponentAttacks = getAnyBuildingsForPlayer(PlayerType.B, building -> building.buildingType == ATTACK, i); + boolean myDefense = getAnyBuildingsForPlayer(PlayerType.A, building -> building.buildingType == DEFENSE, i); + + if (opponentAttacks && !myDefense) { + return true; + } + } + return false; + } + + /** + * Do nothing command + * + * @return the result + **/ + private String doNothingCommand() { + return NOTHING_COMMAND; + } + + /** + * Place building in row + * + * @param buildingType the building type + * @param y the y + * @return the result + **/ + private String placeBuildingInRow(BuildingType buildingType, int y) { + List emptyCells = gameState.getGameMap().stream() + .filter(c -> c.getBuildings().isEmpty() + && c.y == y + && c.x < (gameWidth / 2) - 1) + .collect(Collectors.toList()); + + if (emptyCells.isEmpty()) { + return buildRandom(); + } + + CellStateContainer randomEmptyCell = getRandomElementOfList(emptyCells); + return buildingType.buildCommand(randomEmptyCell.x, randomEmptyCell.y); + } + + /** + * Get random element of list + * + * @param list the list < t > + * @return the result + **/ + private T getRandomElementOfList(List list) { + return list.get((new Random()).nextInt(list.size())); + } + + private boolean getAnyBuildingsForPlayer(PlayerType playerType, Predicate filter, int y) { + return buildings.stream() + .filter(b -> b.getPlayerType() == playerType + && b.getY() == y) + .anyMatch(filter); + } + + /** + * Can afford building + * + * @param buildingType the building type + * @return the result + **/ + private boolean canAffordBuilding(BuildingType buildingType) { + return myself.energy >= gameDetails.buildingsStats.get(buildingType).price; + } +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java new file mode 100644 index 0000000..79ca286 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/Main.java @@ -0,0 +1,46 @@ +package za.co.entelect.challenge; + +import com.google.gson.Gson; +import za.co.entelect.challenge.entities.GameState; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class Main { + private static final String COMMAND_FILE_NAME = "command.txt"; + private static final String STATE_FILE_NAME = "state.json"; + + /** + * 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) { + e.printStackTrace(); + } + + Gson gson = new Gson(); + GameState gameState = gson.fromJson(state, GameState.class); + + Bot bot = new Bot(gameState); + String command = bot.run(); + + writeBotResponseToFile(command); + } + + /** + * Write bot response to file + * @param command the command + **/ + private static void writeBotResponseToFile(String command) { + try { + Files.write(Paths.get(COMMAND_FILE_NAME), command.getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java new file mode 100644 index 0000000..49deaaf --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Building.java @@ -0,0 +1,18 @@ +package za.co.entelect.challenge.entities; + +import za.co.entelect.challenge.enums.BuildingType; +import za.co.entelect.challenge.enums.PlayerType; + +public class Building extends Cell { + + public int health; + public int constructionTimeLeft; + public int price; + public int weaponDamage; + public int weaponSpeed; + public int weaponCooldownTimeLeft; + public int weaponCooldownPeriod; + public int destroyScore; + public int energyGeneratedPerTurn; + public BuildingType buildingType; +} 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/Cell.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Cell.java new file mode 100644 index 0000000..12c8b9d --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Cell.java @@ -0,0 +1,36 @@ +package za.co.entelect.challenge.entities; + +import za.co.entelect.challenge.enums.PlayerType; + +public class Cell { + + protected int x; + protected int y; + protected PlayerType playerType; + + public Cell() { + + } + + public Cell(int x, int y, PlayerType playerType) { + this.x = x; + this.y = y; + this.playerType = playerType; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public PlayerType getPlayerType() { + return playerType; + } + + public boolean isPlayers(PlayerType id) { + return id == playerType; + } +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java new file mode 100644 index 0000000..0b42fc0 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/CellStateContainer.java @@ -0,0 +1,31 @@ +package za.co.entelect.challenge.entities; + +import za.co.entelect.challenge.enums.PlayerType; + +import java.util.ArrayList; +import java.util.List; + +public class CellStateContainer { + public int x; + public int y; + public PlayerType cellOwner; + protected List buildings; + protected List missiles; + + public CellStateContainer(int x, int y, PlayerType cellOwner) { + this.x = x; + this.y = y; + this.cellOwner = cellOwner; + this.buildings = new ArrayList<>(); + this.missiles = new ArrayList<>(); + } + + public List getBuildings() { + return this.buildings; + } + + public List getMissiles() { + return this.missiles; + } + +} 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 new file mode 100644 index 0000000..68a56e7 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameDetails.java @@ -0,0 +1,14 @@ +package za.co.entelect.challenge.entities; + +import za.co.entelect.challenge.enums.BuildingType; + +import java.util.HashMap; + +public class GameDetails { + public int round; + public int mapWidth; + public int mapHeight; + public int roundIncomeEnergy; + public HashMap buildingsStats = new HashMap<>(); + +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java new file mode 100644 index 0000000..2c4aa6c --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/GameState.java @@ -0,0 +1,29 @@ +package za.co.entelect.challenge.entities; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class GameState { + protected Player[] players; + protected CellStateContainer[][] gameMap; + protected GameDetails gameDetails; + + public List getPlayers() { + return new ArrayList<>(Arrays.asList(players)); + } + + public List getGameMap() { + ArrayList list = new ArrayList<>(); + + for (CellStateContainer[] aGameMap : gameMap) { + list.addAll(Arrays.asList(aGameMap)); + } + + return list; + } + + public GameDetails getGameDetails() { + return gameDetails; + } +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java new file mode 100644 index 0000000..344cbd2 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Missile.java @@ -0,0 +1,6 @@ +package za.co.entelect.challenge.entities; + +public class Missile extends Cell { + private int damage; + private int speed; +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java new file mode 100644 index 0000000..a535f44 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/entities/Player.java @@ -0,0 +1,11 @@ +package za.co.entelect.challenge.entities; + +import za.co.entelect.challenge.enums.PlayerType; + +public class Player { + public PlayerType playerType; + public int energy; + public int health; + public int hitsTaken; + public int score; +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java new file mode 100644 index 0000000..4dc788a --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/BuildingType.java @@ -0,0 +1,22 @@ +package za.co.entelect.challenge.enums; + +public enum BuildingType { + DEFENSE(0), + ATTACK(1), + ENERGY(2); + + private int type; + + BuildingType(int type) { + + this.type = type; + } + + public int getType() { + return type; + } + + public String buildCommand(int x, int y) { + return String.format("%d,%d,%d", x, y, getType()); + } +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java new file mode 100644 index 0000000..70275ce --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/Direction.java @@ -0,0 +1,16 @@ +package za.co.entelect.challenge.enums; + +public enum Direction { + LEFT(-1), + RIGHT(1); + + private int multiplier; + + Direction(int multiplier) { + this.multiplier = multiplier; + } + + public int getMultiplier() { + return multiplier; + } +} diff --git a/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java new file mode 100644 index 0000000..cf95ea4 --- /dev/null +++ b/starter-pack/starter-bots/java/src/main/java/za/co/entelect/challenge/enums/PlayerType.java @@ -0,0 +1,6 @@ +package za.co.entelect.challenge.enums; + +public enum PlayerType { + A, + B +} diff --git a/starter-pack/starter-bots/javascript/StarterBot.js b/starter-pack/starter-bots/javascript/StarterBot.js new file mode 100644 index 0000000..d71130e --- /dev/null +++ b/starter-pack/starter-bots/javascript/StarterBot.js @@ -0,0 +1,174 @@ +"use strict"; + +let fs = require('fs'); + +let commandFileName = "command.txt"; +let stateFileName = "state.json"; + +let key = ""; +let workingDirectory = ""; +let stateFile = ""; +let myself = ""; +let opponent = ""; +let gameMap = ""; +let mapSize = ""; +let cells = ""; +let buildings = ""; +let missiles = ""; +let buildingStats = []; + +// Capture the arguments +initBot(process.argv.slice(2)); + +function initBot(args) { + key = args[0]; + workingDirectory = args[1]; + + // Read the current state and choose an action + stateFile = require('./' + stateFileName); + + myself = stateFile.players.filter(p => p.playerType === 'A')[0]; + opponent = stateFile.players.filter(p => p.playerType === 'B')[0]; + mapSize = { + x: stateFile.gameDetails.mapWidth, + y: stateFile.gameDetails.mapHeight + }; + + let stats = stateFile.gameDetails.buildingsStats; + buildingStats[0]= stats.DEFENSE; + buildingStats[1]= stats.ATTACK; + buildingStats[2]= stats.ENERGY; + + gameMap = stateFile.gameMap; + initEntities(); + + runStrategy(); +} + +function initEntities() { + cells = flatMap(gameMap); // all cells on the entire map + + buildings = cells.filter(cell => cell.buildings.length > 0).map(cell => cell.buildings); + buildings = flatMap(buildings); // flat array of everyone's buildings + + missiles = cells.filter(cell => cell.missiles.length > 0).map(cell => cell.missiles); + missiles = flatMap(missiles); // flat array of everyone's missiles +} + +function runStrategy() { + if (isUnderAttack()) { + defendRow(); + } else if (hasEnoughEnergyForMostExpensiveBuilding()) { + buildRandom(); + } else { + doNothingCommand(); + } +} + +function isUnderAttack() { + // is there a row under attack? and have enough energy to build defence? + let myDefenders = buildings.filter(b => b.playerType == 'A' && b.buildingType == 'DEFENSE'); + 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 >= buildingStats[0].price); +} + +function defendRow() { + // is there a row under attack? and have enough energy to build defence? + let myDefenders = buildings.filter(b => b.playerType == 'A' && b.buildingType == 'DEFENSE'); + let opponentAttackers = buildings.filter(b => b.playerType == 'B' && b.buildingType == 'ATTACK') + .filter(b => !myDefenders.some(d => d.y == b.y)); + if (opponentAttackers.length == 0) { + buildRandom(); + return + } + // choose the first row with an opponent attacker + let rowNumber = opponentAttackers[0].y; + // get all the x-coordinates for this row, that are empty + let emptyCells = cells.filter(c => c.buildings.length == 0 && c.x <= (mapSize.x / 2) - 1 && c.y == rowNumber); + if (emptyCells.length == 0) { + // cannot build there, try to build somewhere else + buildRandom(); + return + } + + let command = {x: '', y: '', bt: ''}; + command.x = getRandomFromArray(emptyCells).x; + command.y = rowNumber; + command.bt = 0; // defence building + buildCommand(command.x, command.y, command.bt); +} + +function hasEnoughEnergyForMostExpensiveBuilding() { + return (myself.energy >= Math.max(...(buildingStats.map(stat => stat.price)))); +} + +function buildRandom() { + // cells without buildings on them, and on my half of the map + let emptyCells = cells.filter(c => c.buildings.length == 0 && c.x <= (mapSize.x / 2) - 1); + if (emptyCells.length == 0) { + doNothingCommand(); + return; + } + let randomCell = getRandomFromArray(emptyCells); + + let command = {x: '', y: '', bt: ''}; + command.x = randomCell.x; + command.y = randomCell.y; + command.bt = getRandomInteger(2); + buildCommand(command.x, command.y, command.bt); +} + +function buildCommand(x, y, bt) { + writeToFile(commandFileName, `${x},${y},${bt}`); +} + +function doNothingCommand() { + writeToFile(commandFileName, ``); +} + +function writeToFile(fileName, payload) { + fs.writeFile('./' + fileName, payload, function (err) { + if (err) { + return console.log(err); + } + // console.log(payload); + }); +} + +/*** + * Returns an array with one less level of nesting + * @param array + * @returns {Array} + */ +function flatMap(array) { + return array.reduce((acc, x) => acc.concat(x), []); +} + +/*** + * Returns a random integer between 0(inclusive) and max(inclusive) + * @param max + * @returns {number} + */ +function getRandomInteger(max) { + return Math.round(Math.random() * max); +} + +/** + * Returns an array that is filled with integers from 0(inclusive) to count(inclusive) + * @param count + * @returns {number[]} + */ +function getArrayRange(count) { + return Array.from({length: count}, (v, i) => i); +} + +/** + * Return a random element from a given array + * @param array + * @returns {*} + */ +function getRandomFromArray(array) { + return array[Math.floor((Math.random() * array.length))]; +} diff --git a/starter-pack/starter-bots/javascript/bot.json b/starter-pack/starter-bots/javascript/bot.json new file mode 100644 index 0000000..c8f852b --- /dev/null +++ b/starter-pack/starter-bots/javascript/bot.json @@ -0,0 +1,8 @@ +{ + "author":"John Doe", + "email":"john.doe@example.com", + "nickName" :"Brendan", + "botLocation": "/", + "botFileName": "StarterBot.js", + "botLanguage": "javascript" +} \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/.gitignore b/starter-pack/starter-bots/kotlin/.gitignore new file mode 100644 index 0000000..68d1895 --- /dev/null +++ b/starter-pack/starter-bots/kotlin/.gitignore @@ -0,0 +1,54 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ +cmake-build-release/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +\.idea/ +*.iml + +target/* diff --git a/starter-pack/starter-bots/kotlin/bot.json b/starter-pack/starter-bots/kotlin/bot.json new file mode 100644 index 0000000..4f8ecb2 --- /dev/null +++ b/starter-pack/starter-bots/kotlin/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "Dmitry", + "botLocation": "/target", + "botFileName": "kotlin-sample-bot-1.0-SNAPSHOT-jar-with-dependencies.jar", + "botLanguage": "kotlin" +} \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/pom.xml b/starter-pack/starter-bots/kotlin/pom.xml new file mode 100644 index 0000000..752c214 --- /dev/null +++ b/starter-pack/starter-bots/kotlin/pom.xml @@ -0,0 +1,129 @@ + + + 4.0.0 + + za.co.entelect.challenge + kotlin-sample-bot + 1.0-SNAPSHOT + + + 1.8 + 1.2.40 + + + + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + compile + compile + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + test-compile + test-compile + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + compile + + + java-test-compile + test-compile + testCompile + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + + + + true + za.co.entelect.challenge.Main + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.6 + + + make-assembly + package + single + + + + za.co.entelect.challenge.Main + + + + jar-with-dependencies + + + + + + + + + + + com.google.code.gson + gson + 2.8.2 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + + + \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt new file mode 100644 index 0000000..7d5cfb0 --- /dev/null +++ b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Bot.kt @@ -0,0 +1,129 @@ +package za.co.entelect.challenge + +import java.util.ArrayList +import java.util.Random + +class Bot(private val gameState: GameState) { + private val gameDetails: GameDetails = gameState.gameDetails + private val gameWidth: Int + private val gameHeight: Int + private val myself: Player + private val opponent: Player + private val buildings: List + private val missiles: List + + //if enemy has an attack building and I don't have a blocking wall + private val isUnderAttack: Boolean + get() { + for (i in 0 until gameHeight) { + val opponentAttacksCount = getAllBuildingsForPlayer(PlayerType.B, ::isAttackBuilding, i).size + val myDefenseCount = getAllBuildingsForPlayer(PlayerType.A, ::isDefenceBuilding, i).size + if (opponentAttacksCount > 0 && myDefenseCount == 0) { + return true + } + } + return false + } + + init { + // energyPerTurn = gameDetails.; + gameWidth = gameDetails.mapWidth + gameHeight = gameDetails.mapHeight + myself = gameState.players.first { p -> p.playerType == PlayerType.A } + opponent = gameState.players.first { p -> p.playerType == PlayerType.B } + buildings = gameState.gameMap.flatten().map { cellStateContainer -> cellStateContainer.buildings }.flatten() + missiles = gameState.gameMap.flatten().map { cellStateContainer -> cellStateContainer.missiles }.flatten() + } + + fun run(): String { + return when { + isUnderAttack -> defendRow() + hasEnoughEnergyForMostExpensiveBuilding() -> buildRandom() + else -> doNothingCommand() + } + } + + private fun buildRandom(): String { + val emptyCells = gameState.gameMap.flatten() + .filter { c -> c.buildings.isEmpty() && c.x < gameWidth / 2 } + if (emptyCells.isEmpty()) { + return doNothingCommand() + } + val randomEmptyCell = getRandomElementOfList(emptyCells) + val buildingTypes = ArrayList(gameDetails.buildingPrices.keys) + val randomBuildingType = getRandomElementOfList(buildingTypes) + return if (!canAffordBuilding(randomBuildingType)) { + doNothingCommand() + } else buildCommand(randomEmptyCell.x, randomEmptyCell.y, randomBuildingType) + } + + private fun hasEnoughEnergyForMostExpensiveBuilding(): Boolean { + return gameDetails.buildingPrices.values.stream() + .filter { bp -> bp < myself.energy } + .toArray().size == 3 + } + + private fun defendRow(): String { + for (i in 0 until gameHeight) { + val opponentAttacksCount = getAllBuildingsForPlayer(PlayerType.B, { b -> b.buildingType == BuildingType.ATTACK }, i).size + if (opponentAttacksCount > 0 && canAffordBuilding(BuildingType.DEFENSE)) { + return placeBuildingInRow(BuildingType.DEFENSE, i) + } + } + return buildRandom() + } + + private fun doNothingCommand(): String { + return "" + } + + private fun placeBuildingInRow(buildingType: BuildingType, y: Int): String { + val emptyCells = gameState.gameMap.flatten() + .filter { + (it.buildings.isEmpty() + && it.y == y + && it.x < gameWidth / 2 - 1) + } + if (emptyCells.isEmpty()) { + return buildRandom() + } + val randomEmptyCell = getRandomElementOfList(emptyCells) + return buildCommand(randomEmptyCell.x, randomEmptyCell.y, buildingType) + } + + private fun getRandomElementOfList(list: List): T { + return list[Random().nextInt(list.size)] + } + + private fun buildCommand(x: Int, y: Int, buildingType: BuildingType): String { + val buildCommand = StringBuilder() + + buildCommand.append(x) + buildCommand.append(",") + buildCommand.append(y) + buildCommand.append(",") + when (buildingType) { + + BuildingType.DEFENSE -> buildCommand.append("0") + BuildingType.ATTACK -> buildCommand.append("1") + BuildingType.ENERGY -> buildCommand.append("2") + } + return buildCommand.toString() + } + + private fun getAllBuildingsForPlayer(playerType: PlayerType, filter: (Building) -> Boolean, y: Int): List { + return buildings + .filter { b -> b.playerType == playerType && b.y == y } + .filter(filter) + } + + private fun canAffordBuilding(buildingType: BuildingType): Boolean { + val cost = when (buildingType) { + BuildingType.DEFENSE -> gameDetails.buildingPrices.getOrDefault(BuildingType.DEFENSE, 100000) + BuildingType.ATTACK -> gameDetails.buildingPrices.getOrDefault(BuildingType.ATTACK, 100000) + BuildingType.ENERGY -> gameDetails.buildingPrices.getOrDefault(BuildingType.ENERGY, 100000) + } + return myself.energy >= cost + } + +} diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt new file mode 100644 index 0000000..a1eb820 --- /dev/null +++ b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Entities.kt @@ -0,0 +1,45 @@ +package za.co.entelect.challenge + +import java.util.HashMap + +data class Building( + val x: Int, val y: Int, val playerType: PlayerType, + val health: Int, + val constructionTimeLeft: Int, + val price: Int, + val weaponDamage: Int, + val weaponSpeed: Int, + val weaponCooldownTimeLeft: Int, + val weaponCooldownPeriod: Int, + val destroyMultiplier: Int, + val constructionScore: Int, + val energyGeneratedPerTurn: Int, + val buildingType: BuildingType) + +data class Missile( + val x: Int, val y: Int, val playerType: PlayerType, + val damage: Int, + val speed: Int) + +data class Player( + val playerType: PlayerType, + val energy: Int, + val health: Int, + val hitsTaken: Int, + val score: Int) + +data class CellStateContainer( + val x: Int, val y: Int, val playerType: PlayerType, + val buildings: List, + val missiles: List) + +data class GameDetails( + val round: Int, + val mapWidth: Int, + val mapHeight: Int, + val buildingPrices: HashMap) + +data class GameState( + val players: Array, + val gameMap: Array>, + var gameDetails: GameDetails) diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt new file mode 100644 index 0000000..d9c9d75 --- /dev/null +++ b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Enums.kt @@ -0,0 +1,25 @@ +package za.co.entelect.challenge + +enum class BuildingType { + DEFENSE, + ATTACK, + ENERGY +} + +fun isAttackBuilding(building: Building) : Boolean { + return building.buildingType == BuildingType.ATTACK +} + +fun isDefenceBuilding(building: Building) : Boolean { + return building.buildingType == BuildingType.DEFENSE +} + +enum class Direction private constructor(val multiplier: Int) { + LEFT(-1), + RIGHT(1) +} + +enum class PlayerType { + A, + B +} \ No newline at end of file diff --git a/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt new file mode 100644 index 0000000..5dbe23f --- /dev/null +++ b/starter-pack/starter-bots/kotlin/src/main/kotlin/za/co/entelect/challenge/Main.kt @@ -0,0 +1,41 @@ +package za.co.entelect.challenge + +import com.google.gson.Gson + +import java.io.* + +object Main { + private const val COMMAND_FILE_NAME = "command.txt" + private const val STATE_FILE_NAME = "state.json" + + @JvmStatic + fun main(args: Array) { + var state = "" + try { + val br = BufferedReader(FileReader(STATE_FILE_NAME)) + state = br.readLine() + } catch (e: IOException) { + e.printStackTrace() + } + + val gson = Gson() + val gameState = gson.fromJson(state, GameState::class.java) + + val bot = Bot(gameState) + val command = bot.run() + + writeBotResponseToFile(command) + } + + private fun writeBotResponseToFile(command: String) { + try { + val bufferedWriter = BufferedWriter(FileWriter(File(COMMAND_FILE_NAME))) + bufferedWriter.write(command) + bufferedWriter.flush() + bufferedWriter.close() + } catch (e: IOException) { + e.printStackTrace() + } + + } +} 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/python3/README.md b/starter-pack/starter-bots/python3/README.md new file mode 100644 index 0000000..e69de29 diff --git a/starter-pack/starter-bots/python3/StarterBot.py b/starter-pack/starter-bots/python3/StarterBot.py new file mode 100644 index 0000000..110eef8 --- /dev/null +++ b/starter-pack/starter-bots/python3/StarterBot.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +''' +Entelect StarterBot for Python3 +''' +import time + +startTime = time.time() + +import json +import os +from time import sleep +import random + + + + +class StarterBot: + + def __init__(self,state_location): + ''' + Initialize Bot. + Load all game state information. + ''' + try: + self.game_state = self.loadState(state_location) + except IOError: + print("Cannot load Game State") + + self.full_map = self.game_state['gameMap'] + self.rows = self.game_state['gameDetails']['mapHeight'] + self.columns = self.game_state['gameDetails']['mapWidth'] + self.command = '' + + self.player_buildings = self.getPlayerBuildings() + self.opponent_buildings = self.getOpponentBuildings() + self.projectiles = self.getProjectiles() + + self.player_info = self.getPlayerInfo('A') + self.opponent_info = self.getPlayerInfo('B') + + self.round = self.game_state['gameDetails']['round'] + + 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 + + + def loadState(self,state_location): + ''' + Gets the current Game State json file. + ''' + return json.load(open(state_location,'r')) + + def getPlayerInfo(self,playerType): + ''' + Gets the player information of specified player type + ''' + for i in range(len(self.game_state['players'])): + if self.game_state['players'][i]['playerType'] == playerType: + return self.game_state['players'][i] + else: + continue + return None + + def getOpponentBuildings(self): + ''' + Looks for all buildings, regardless if completed or not. + 0 - Nothing + 1 - Attack Unit + 2 - Defense Unit + 3 - Energy Unit + ''' + opponent_buildings = [] + + for row in range(0,self.rows): + buildings = [] + for col in range(int(self.columns/2),self.columns): + if (len(self.full_map[row][col]['buildings']) == 0): + buildings.append(0) + elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ATTACK'): + buildings.append(1) + elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'DEFENSE'): + buildings.append(2) + elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ENERGY'): + buildings.append(3) + else: + buildings.append(0) + + opponent_buildings.append(buildings) + + return opponent_buildings + + def getPlayerBuildings(self): + ''' + Looks for all buildings, regardless if completed or not. + 0 - Nothing + 1 - Attack Unit + 2 - Defense Unit + 3 - Energy Unit + ''' + player_buildings = [] + + for row in range(0,self.rows): + buildings = [] + for col in range(0,int(self.columns/2)): + if (len(self.full_map[row][col]['buildings']) == 0): + buildings.append(0) + elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ATTACK'): + buildings.append(1) + elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'DEFENSE'): + buildings.append(2) + elif (self.full_map[row][col]['buildings'][0]['buildingType'] == 'ENERGY'): + buildings.append(3) + else: + buildings.append(0) + + player_buildings.append(buildings) + + return player_buildings + + def getProjectiles(self): + ''' + Find all projectiles on the map. + 0 - Nothing there + 1 - Projectile belongs to player + 2 - Projectile belongs to opponent + ''' + projectiles = [] + + for row in range(0,self.rows): + temp = [] + for col in range(0,self.columns): + if (len(self.full_map[row][col]['missiles']) == 0): + temp.append(0) + elif (self.full_map[row][col]['missiles'][0]['playerType'] == 'A'): + temp.append(1) + elif (self.full_map[row][col]['missiles'][0]['playerType'] == 'B'): + temp.append(2) + + projectiles.append(temp) + + return projectiles + + + def checkDefense(self, lane_number): + + ''' + Checks a lane. + Returns True if lane contains defense unit. + ''' + + lane = list(self.opponent_buildings[lane_number]) + if (lane.count(2) > 0): + return True + else: + return False + + def checkMyDefense(self, lane_number): + + ''' + Checks a lane. + Returns True if lane contains defense unit. + ''' + + lane = list(self.player_buildings[lane_number]) + if (lane.count(2) > 0): + return True + else: + return False + + def checkAttack(self, lane_number): + + ''' + Checks a lane. + Returns True if lane contains attack unit. + ''' + + lane = list(self.opponent_buildings[lane_number]) + if (lane.count(1) > 0): + return True + else: + return False + + def getUnOccupied(self,lane): + ''' + Returns index of all unoccupied cells in a lane + ''' + indexes = [] + for i in range(len(lane)): + if lane[i] == 0 : + indexes.append(i) + + return indexes + + + def generateAction(self): + ''' + Place your bot logic here ! + + - If there is an opponent attack unit on a row, and you have enough energy for a defense + Build a defense at a random unoccupied location on that row if it is undefended. + - Else If you have enough energy for the most expensive building + Build a random building type at a random unoccupied location + - Else: + Save energy until you have enough for the most expensive building + + Building Types : + 0 : Defense Building + 1 : Attack Building + 2 : Energy Building + ''' + lanes = [] + x,y,building = 0,0,0 + #check all lanes for an attack unit + for i in range(self.rows): + 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.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 + #A count of 0 would mean all lanes are not under attack + if (len(lanes) > 0) : + #Chose a random lane under attack to place a defensive unit + #Chose a cell that is unoccupied in that lane + building = 0 + y = random.choice(lanes) + 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(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-1) + y = random.randint(0,int(self.columns/2)-1) + else: + self.writeDoNothing() + return None + + self.writeCommand(x,y,building) + return x,y,building + + def writeCommand(self,x,y,building): + ''' + command in form : x,y,building_type + ''' + outfl = open('command.txt','w') + outfl.write(','.join([str(x),str(y),str(building)])) + outfl.close() + return None + + def writeDoNothing(self): + ''' + command in form : x,y,building_type + ''' + outfl = open('command.txt','w') + outfl.write("") + outfl.close() + return None + +if __name__ == '__main__': + s = StarterBot('state.json') + s.generateAction() + \ No newline at end of file diff --git a/starter-pack/starter-bots/python3/bot.json b/starter-pack/starter-bots/python3/bot.json new file mode 100644 index 0000000..fd7b285 --- /dev/null +++ b/starter-pack/starter-bots/python3/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "Guido", + "botLocation": "/", + "botFileName": "StarterBot.py", + "botLanguage": "python3" +} \ No newline at end of file diff --git a/starter-pack/starter-bots/rust/.gitignore b/starter-pack/starter-bots/rust/.gitignore new file mode 100644 index 0000000..44ba2ac --- /dev/null +++ b/starter-pack/starter-bots/rust/.gitignore @@ -0,0 +1,10 @@ +target +command.txt +state.json + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk \ No newline at end of file diff --git a/starter-pack/starter-bots/rust/Cargo.toml b/starter-pack/starter-bots/rust/Cargo.toml new file mode 100644 index 0000000..a1ac517 --- /dev/null +++ b/starter-pack/starter-bots/rust/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "entelect_challenge_rust_sample" +version = "1.0.0" + +[dependencies] +serde_derive = "1.0.43" +serde = "1.0.43" +serde_json = "1.0.16" + +rand = "0.4.2" diff --git a/starter-pack/starter-bots/rust/README.md b/starter-pack/starter-bots/rust/README.md new file mode 100644 index 0000000..0b97c14 --- /dev/null +++ b/starter-pack/starter-bots/rust/README.md @@ -0,0 +1,38 @@ +# Rust Sample Bot + +Rust is a systems programming language, giving programmers the low +level control that they would usually associate with a programming +langauge like C or C++, but modern high level programming features. + +Rust is a compiled language, which compiles to an +architecture-specific binary. + +For getting started with this bot in particular, I've done a write up +about [writing a Rust bot for the Entelect challenge](https://www.worthe-it.co.za/programming/2018/05/02/writing-an-entelect-challenge-bot-in-rust.html). + +## Environment Setup + +The Rust compiler toolchain can be downloaded from the Rust project +website. + +https://www.rust-lang.org/en-US/install.html + +## Compilation + +The bot can be built using the Rust build tool, Cargo. For the sake of +the competition, the `--release` flag should be used. + +``` +cargo build --release +``` + +## Running + +After compilation, there will be an executable in +`target/release/`. + +For example, this sample bot's name is +`entelect_challenge_rust_sample`, so the executable to be run is +`target/release/entelect_challenge_rust_sample` on Linux or +`target/release/entelect_challenge_rust_sample.exe` on Windows. + diff --git a/starter-pack/starter-bots/rust/bot.json b/starter-pack/starter-bots/rust/bot.json new file mode 100644 index 0000000..4aabc28 --- /dev/null +++ b/starter-pack/starter-bots/rust/bot.json @@ -0,0 +1,8 @@ +{ + "author": "John Doe", + "email": "john.doe@example.com", + "nickName": "Graydon", + "botLocation": "/target/release/", + "botFileName": "entelect_challenge_rust_sample", + "botLanguage": "rust" +} diff --git a/starter-pack/starter-bots/rust/src/main.rs b/starter-pack/starter-bots/rust/src/main.rs new file mode 100644 index 0000000..0029419 --- /dev/null +++ b/starter-pack/starter-bots/rust/src/main.rs @@ -0,0 +1,168 @@ +extern crate serde; +extern crate serde_json; + +#[macro_use] +extern crate serde_derive; + +extern crate rand; +use rand::{thread_rng, Rng}; + +use std::error::Error; + +#[repr(u8)] +#[derive(Debug, Clone, Copy)] +enum Building { + Defense = 0, + Attack = 1, + Energy = 2, +} + +impl std::fmt::Display for Building { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", *self as u8) + } +} + +const STATE_PATH: &str = "state.json"; + +const COMMAND_PATH: &str = "command.txt"; + +#[derive(Debug, Clone, Copy)] +struct Command { + x: u32, + y: u32, + building: Building, +} + +impl std::fmt::Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{},{},{}", self.x, self.y, self.building) + } +} + +mod state; + +fn current_energy(state: &state::State) -> u32 { + state.players.iter() + .filter(|p| p.player_type == 'A') + .map(|p| p.energy) + .next() + .unwrap_or(0) +} + +fn can_afford_all_buildings(state: &state::State) -> bool { + can_afford_attack_buildings(state) && + can_afford_defence_buildings(state) && + can_afford_energy_buildings(state) +} + +fn can_afford_attack_buildings(state: &state::State) -> bool { + current_energy(state) >= state.game_details.building_prices.attack +} +fn can_afford_defence_buildings(state: &state::State) -> bool { + current_energy(state) >= state.game_details.building_prices.defense +} +fn can_afford_energy_buildings(state: &state::State) -> bool { + current_energy(state) >= state.game_details.building_prices.energy +} + +fn is_under_attack(state: &state::State, y: u32) -> bool { + let attack = state.game_map[y as usize].iter() + .any(|cell| cell.buildings.iter() + .any(|building| building.player_type == 'B' && + building.building_type == "ATTACK")); + let defences = state.game_map[y as usize].iter() + .any(|cell| cell.buildings.iter() + .any(|building| building.player_type == 'A' && + building.building_type == "DEFENSE")); + attack && !defences +} + +fn is_occupied(state: &state::State, x: u32, y: u32) -> bool { + !state.game_map[y as usize][x as usize].buildings.is_empty() +} + +fn unoccupied_in_row(state: &state::State, y: u32) -> Vec { + (0..state.game_details.map_width/2) + .filter(|&x| !is_occupied(&state, x, y)) + .collect() +} + +fn unoccupied_cells(state: &state::State) -> Vec<(u32, u32)> { + (0..state.game_details.map_width/2) + .flat_map(|x| (0..state.game_details.map_height) + .map(|y| (x, y)) + .collect::>()) + .filter(|&(x, y)| !is_occupied(&state, x, y)) + .collect() +} + +fn choose_move(state: &state::State) -> Option { + let mut rng = thread_rng(); + + if can_afford_defence_buildings(state) { + for y in 0..state.game_details.map_height { + if is_under_attack(state, y) { + let x_options = unoccupied_in_row(state, y); + if let Some(&x) = rng.choose(&x_options) { + return Some(Command { + x: x, + y: y, + building: Building::Defense + }); + } + } + } + } + + if can_afford_all_buildings(state) { + let options = unoccupied_cells(state); + let option = rng.choose(&options); + let buildings = [Building::Attack, Building::Defense, Building::Energy]; + let building = rng.choose(&buildings); + match (option, building) { + (Some(&(x, y)), Some(&building)) => Some(Command { + x: x, + y: y, + building: building + }), + _ => None + } + } + else { + None + } +} + +use std::fs::File; +use std::io::prelude::*; + +fn write_command(filename: &str, command: Option) -> Result<(), Box > { + let mut file = File::create(filename)?; + if let Some(command) = command { + write!(file, "{}", command)?; + } + + Ok(()) +} + +use std::process; + +fn main() { + let state = match state::read_state_from_file(STATE_PATH) { + Ok(state) => state, + Err(error) => { + eprintln!("Failed to read the {} file. {}", STATE_PATH, error); + process::exit(1); + } + }; + let command = choose_move(&state); + + match write_command(COMMAND_PATH, command) { + Ok(()) => {} + Err(error) => { + eprintln!("Failed to write the {} file. {}", COMMAND_PATH, error); + process::exit(1); + } + } +} diff --git a/starter-pack/starter-bots/rust/src/state.rs b/starter-pack/starter-bots/rust/src/state.rs new file mode 100644 index 0000000..429db6d --- /dev/null +++ b/starter-pack/starter-bots/rust/src/state.rs @@ -0,0 +1,86 @@ +use std::fs::File; +use std::io::prelude::*; +use serde_json; +use std::error::Error; + +pub fn read_state_from_file(filename: &str) -> Result> { + let mut file = File::open(filename)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; + let state = serde_json::from_str(content.as_ref())?; + Ok(state) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct State { + pub game_details: GameDetails, + pub players: Vec, + pub game_map: Vec>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GameDetails { + pub round: u32, + pub map_width: u32, + pub map_height: u32, + pub building_prices: BuildingPrices +} + +#[derive(Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub struct BuildingPrices { + pub energy: u32, + pub defense: u32, + pub attack: u32 +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Player { + pub player_type: char, + pub energy: u32, + pub health: u32, + pub hits_taken: u32, + pub score: u32 +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GameCell { + pub x: u32, + pub y: u32, + pub buildings: Vec, + pub missiles: Vec, + pub cell_owner: char +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildingState { + pub health: u32, + pub construction_time_left: i32, + pub price: u32, + pub weapon_damage: u32, + pub weapon_speed: u32, + pub weapon_cooldown_time_left: u32, + pub weapon_cooldown_period: u32, + pub destroy_multiplier: u32, + pub construction_score: u32, + pub energy_generated_per_turn: u32, + pub building_type: String, + pub x: u32, + pub y: u32, + pub player_type: char +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MissileState { + pub damage: u32, + pub speed: u32, + pub x: u32, + pub y: u32, + pub player_type: char +} From 0fda0f406942498d1cc9f306d82d8e291dc8b34b Mon Sep 17 00:00:00 2001 From: "edwin.fullard" Date: Fri, 11 May 2018 21:48:23 +0200 Subject: [PATCH 19/20] Fix new languages bot locations and add verbose-mode property --- starter-pack/ReadMe.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/starter-pack/ReadMe.txt b/starter-pack/ReadMe.txt index 9858291..88781f3 100644 --- a/starter-pack/ReadMe.txt +++ b/starter-pack/ReadMe.txt @@ -52,6 +52,8 @@ The format of the 'config.json' is as follows: "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" & @@ -149,12 +151,12 @@ Kotlin bots 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/kotlin] + [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/kotlin] + [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/kotlin] + [https://github.com/EntelectChallenge/2018-TowerDefence/tree/master/starter-bots/php] From c1d9fdc6617546a3e96e48fcd082f4b6e7754d29 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 11 May 2018 22:12:51 +0200 Subject: [PATCH 20/20] Added verbose as a setting. /verbose-toggle: Auto stash before rebase of "origin/develop" Revert "/verbose-toggle: Auto stash before rebase of "origin/develop"" This reverts commit 39d69ad9790aaee7a312d8ace818747dd5812d65. Added correct default mode. --- game-runner/config.json | 1 + game-runner/pom.xml | 15 ++++++ .../challenge/bootstrapper/Config.java | 3 ++ .../bootstrapper/GameBootstrapper.java | 46 +++++++++++-------- .../engine/runner/GameEngineRunner.java | 9 +++- .../engine/runner/RunnerRoundProcessor.java | 7 ++- .../entelect/challenge/player/BotPlayer.java | 23 ++++++---- .../challenge/player/ConsolePlayer.java | 21 +++++---- .../src/main/resources/log4j2.properties | 12 +++++ 9 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 game-runner/src/main/resources/log4j2.properties diff --git a/game-runner/config.json b/game-runner/config.json index 4aa36cd..3bfe8ec 100644 --- a/game-runner/config.json +++ b/game-runner/config.json @@ -1,6 +1,7 @@ { "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/java", "player-b": "../reference-bot/java" diff --git a/game-runner/pom.xml b/game-runner/pom.xml index bc1c20b..8961470 100644 --- a/game-runner/pom.xml +++ b/game-runner/pom.xml @@ -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 e6ef365..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 @@ -17,4 +17,7 @@ public class Config { @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 dbda3a8..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,11 +28,13 @@ 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 { @@ -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/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/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