diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..05dbb3f1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Halite Contributing Guide + +If you find a bug or have a feature request, please [open an issue](https://github.com/HaliteChallenge/Halite/issues/new). + +Want to help out? Have you implemented a patch or a new feature? Send us a pull request! If you are looking for things to do, check out [our open issues](https://github.com/HaliteChallenge/Halite/issues). + +## Common Contributions + +### Writing a Starter Package + +If you'd like to write a starter package for a new language, see this [guide](https://halite.io/advanced_writing_sp.php). + +### Adding Your Company or University + +Edit [this whitelist](https://github.com/HaliteChallenge/Halite/edit/master/website/organizationWhitelist.txt) and send us a pull request. If you need to change your email, head [here](https://halite.io/email.php). We'll make sure to tag all members of your organization who have already signed up. + +## Folder Contents + +- `admin/` - A collection of administrative resources (ex. a technical specification) +- `airesources/` - The language-specific starter kits for writing bots +- `environment/` - The halite game engine +- `tests/` - All of the project's unit and integration tests +- `website/` - The website that hosts the competition. Includes the API that manages the game servers. +- `worker/` - The source for the worker servers that compile bots and run games safely + +## Installing the website on Ubuntu + +Clone the repo: + + git clone https://github.com/HaliteChallenge/Halite.git + +Run the website install script with root user permissions. This will install php, apache, python (and some python modules), and composer (and some composer packages): + + cd website; sudo ./install.sh + +Run the database install script with root user permissions. This will install mysql and insert our schema into a database titled Halite. + + cd sql; sudo ./install.sh + +Create a `halite.ini` file using your favorite text editor in the root project directory. Place information about your local database setup in there. Your `halite.ini` file should look like this; + + [database] + hostname = 127.0.0.1 + username = YOUR_LOCAL_MYSQL_USERNAME + password = YOUR_LOCAL_MYSQL_PASSWORD + name = Halite diff --git a/README.md b/README.md index ecf79990..87b4cff5 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,16 @@ Halite is a programming competition. Contestants write bots to play an original multi-player turn-based strategy game played on a rectangular grid. For more information about the game, visit [our website](http://halite.io). -## Folder Contents - -- `admin/` - A collection of administrative resources (ex. a technical specification) -- `airesources/` - The language-specific starter kits for writing bots -- `environment/` - The halite game engine -- `tests/` - All of the project's unit and integration tests -- `website/` - The website that hosts the competition. Includes the API that manages the game servers. -- `worker/` - The source for the worker servers that compile bots and run games safely - ## Contributing -If you find a bug or have a feature request, please [open an issue](https://github.com/HaliteChallenge/Halite/issues/new). +See [the Contributing Guide](CONTRIBUTING.md). + +## Questions -Want to help out? Have you implemented a patch or a new feature? Send us a pull request! If you are looking for things to do, check out [our open issues](https://github.com/HaliteChallenge/Halite/issues). +See [the Forums](http://forums.halite.io) and [our Discord chat](https://discordapp.com/invite/rbVDB4n). -If you'd like to write a starter package for a new language, see this [guide](https://halite.io/advanced_writing_sp.php). +## Authors -### Authors +Halite was primarily created by [Ben Spector](https://github.com/Sydriax) and [Michael Truell](https://github.com/truell20) for Two Sigma during their summer 2016 internship. -Halite was primarily created by [Ben Spector](https://github.com/Sydriax) and [Michael Truell](https://github.com/truell20) for Two Sigma during their summer 2016 internship. However, many others contributed to Halite's developement, including [Matt Adereth](https://github.com/adereth) and [Trammell Hudson](https://github.com/osresearch) from Two Sigma and [Arnaud Sahuguet](https://github.com/sahuguet) and [Scot Spinner](https://github.com/awesomescot) from Cornell Tech. +Many others contributed to Halite's developement, including [Matt Adereth](https://github.com/adereth), [Trammell Hudson](https://github.com/osresearch), and Jaques Clapauch from Two Sigma and [Arnaud Sahuguet](https://github.com/sahuguet) and [Scot Spinner](https://github.com/awesomescot) from Cornell Tech. Halite's participants, including [Nick Malaguti](https://github.com/nmalaguti), [Travis Erdman](https://github.com/erdman), and [Janzert](https://github.com/janzert), have also been instrumental to the project. diff --git a/admin/checkTaskCompletion.py b/admin/checkTaskCompletion.py new file mode 100755 index 00000000..c3469185 --- /dev/null +++ b/admin/checkTaskCompletion.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import configparser +import time +from datetime import datetime + +import pymysql + +parser = configparser.ConfigParser() +parser.read("../halite.ini") + +DB_CONFIG = parser["database"] + +def check_compiles(db): + db.begin() + with db.cursor() as cursor: + cursor.execute("SELECT COUNT(*) FROM User WHERE compileStatus != 0") + return cursor.fetchone()['COUNT(*)'] + +def check_workers(db, start_time): + db.begin() + with db.cursor() as cursor: + cursor.execute("SELECT workerID, lastRequestTime FROM Worker ORDER BY workerID") + workers = cursor.fetchall() + waiting = list() + for w in workers: + if w["lastRequestTime"] < start_time: + waiting.append(w) + return waiting + +def main(): + if ("compState" not in parser or "noGameTasks" not in parser["compState"] or + not parser["compState"]["noGameTasks"]): + print(parser["compState"]["noGameTasks"]) + print("Game tasks still activated. Disable in halite.ini [compState] noGameTasks") + return + start_time = datetime.now() + db = pymysql.connect(host=DB_CONFIG['hostname'], user=DB_CONFIG['username'], passwd=DB_CONFIG['password'], db=DB_CONFIG['name'], cursorclass=pymysql.cursors.DictCursor) + + compiles = 1 + workers = [1] + while compiles or workers: + compiles = check_compiles(db) + workers = check_workers(db, start_time) + if compiles: + print("Waiting for %d more compiles to complete." % (compiles,)) + if workers: + print("Waiting for workers: ", end="") + print(", ".join(str(w["workerID"]) for w in workers[:5]), end="") + if len(workers) > 5: + print(" and %d more" % (len(workers) - 5,)) + else: + print() + time.sleep(5) + db.begin() + with db.cursor() as cursor: + cursor.execute("SELECT MAX(gameID) FROM Game") + max_game = cursor.fetchone()["MAX(gameID)"] + print("All tasks completed, last gameID %d." % (max_game,)) + +if __name__ == "__main__": + main() diff --git a/admin/commandRunner.py b/admin/commandRunner.py index 5fd9f891..982bfa8d 100644 --- a/admin/commandRunner.py +++ b/admin/commandRunner.py @@ -7,7 +7,7 @@ def runOnWorker(worker, keyPath, command): print("########"+worker['ipAddress']+"########") - os.system("ssh -i \""+keyPath+"\" ubuntu@"+worker['ipAddress']+" '"+command+"'") + os.system("ssh -oStrictHostKeyChecking=no -i \""+keyPath+"\" ubuntu@"+worker['ipAddress']+" '"+command+"'") print("########"+worker['ipAddress']+"########") parser = configparser.ConfigParser() diff --git a/admin/cron/workerChecker.py b/admin/cron/workerChecker.py index 08199316..2ce2346d 100755 --- a/admin/cron/workerChecker.py +++ b/admin/cron/workerChecker.py @@ -21,6 +21,4 @@ print("All good!") else: message = "Some workers haven't communicated with the manager in a while!

" - for res in results: - message += "Worker with an id of "+str(res[0])+" and an api key "+str(res[1])+" hasn't contacted the manager for over 30 minutes.
" - haliteEmailer.sendEmail(HALITE_EMAIL, HALITE_EMAIL_PASSWORD, "WORKER ALERT", message, HALITE_EMAIL) + haliteEmailer.sendEmail(HALITE_EMAIL, HALITE_EMAIL_PASSWORD, str(len(results))+" workers down", message, HALITE_EMAIL) diff --git a/admin/md/INSTALL.md b/admin/md/INSTALL.md index 9f7a0642..298a7215 100644 --- a/admin/md/INSTALL.md +++ b/admin/md/INSTALL.md @@ -23,7 +23,6 @@ Finish Apache setup: * [Allow the following of symlinks by apache](http://superuser.com/questions/244245/how-do-i-get-apache-to-follow-symlinks) * [Allow .htaccess override](http://stackoverflow.com/a/22526144) * [Redirect root directory to website directory](http://serverfault.com/questions/9992/how-to-get-apache2-to-redirect-to-a-subdirectory) - * Tell apache to forbid access to the storage/errors and storage/bots folders and to the halite.ini file * [Increase your max file upload size (worker's posting large replays, users posting big bot archives)](http://stackoverflow.com/questions/2184513/php-change-the-maximum-upload-file-size) To setup automatic backups on the website server, copy the `backupWebsite` file in the `Halite/website/cron` folder into the server's `/etc/cron.hourly` folder, change the IP address in the file to that of the backup server, and make sure that the ssh key of the website server is in the `~/.ssh/authorized_keys` file on the backup server. Once copied, mark the `backupWebsite` file as executable and give cron permission to execute it like so: diff --git a/admin/rankReset.py b/admin/rankReset.py new file mode 100755 index 00000000..ea446e63 --- /dev/null +++ b/admin/rankReset.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import configparser +import pymysql + +parser = configparser.ConfigParser() +parser.read("../halite.ini") + +DB_CONFIG = parser["database"] + +def main(): + confirm = input("This will clear all current ranks, are you sure? [y/N] ") + if confirm != "y": + return + db = pymysql.connect(host=DB_CONFIG["hostname"], user=DB_CONFIG['username'], passwd=DB_CONFIG['password'], db=DB_CONFIG['name'], cursorclass=pymysql.cursors.DictCursor) + cursor = db.cursor() + cursor.execute("SELECT COUNT(*) FROM User WHERE isRunning=1") + num_active = cursor.fetchone()['COUNT(*)'] + cursor.execute("INSERT INTO UserHistory (userID, versionNumber, lastRank, lastNumPlayers, lastNumGames) SELECT userID, numSubmissions, rank, %d, numGames FROM User WHERE isRunning=1" % (num_active,)) + cursor.execute("UPDATE User SET numSubmissions=numSubmissions+1, numGames=0, mu=25.0, sigma=8.333 WHERE isRunning=1") + db.commit() + db.close() + print("All ranks successfully reset.") + +if __name__ == "__main__": + main() diff --git a/admin/updateOrgs.py b/admin/updateOrgs.py index 89eed20d..57dbaba9 100644 --- a/admin/updateOrgs.py +++ b/admin/updateOrgs.py @@ -20,12 +20,15 @@ continue realUserOrg = "Other" - emailDomain = user["email"].split("@")[1] + try: + emailDomain = user["email"].split("@")[1] + except: + pass for org in orgs: if emailDomain == org[1]: realUserOrg = org[0] break - if realUserOrg != "Other" and realUserOrg != user["organization"]: - print(user["organization"] + " " + realUserOrg) + if (realUserOrg != "Other" or user["organization"] == "") and realUserOrg != user["organization"]: + print("%s, %s, %s" % (realUserOrg, user["organization"], user["email"])) cursor.execute("update User set organization = '"+realUserOrg+"' where userID="+str(user["userID"])) db.commit() diff --git a/airesources/C/MyBot.c b/airesources/C/MyBot.c new file mode 100644 index 00000000..6ce5c9a0 --- /dev/null +++ b/airesources/C/MyBot.c @@ -0,0 +1,36 @@ +#include +#include +#include + +#include "hlt.h" + +#define BOT_NAME "MyCBot" + +int main(void) { + + GAME game; + int x, y, direction; + + srand(time(NULL)); + + game = GetInit(); + SendInit(BOT_NAME); + + while (1) { + + GetFrame(game); + + for (x = 0 ; x < game.width ; x++) { + for (y = 0 ; y < game.height ; y++) { + if (game.owner[x][y] == game.playertag) { + direction = rand() % 5; + SetMove(game, x, y, direction); + } + } + } + + SendFrame(game); + } + + return 0; +} diff --git a/airesources/C/RandomBot.c b/airesources/C/RandomBot.c new file mode 100644 index 00000000..756fae13 --- /dev/null +++ b/airesources/C/RandomBot.c @@ -0,0 +1,36 @@ +#include +#include +#include + +#include "hlt.h" + +#define BOT_NAME "RandomCBot" + +int main(void) { + + GAME game; + int x, y, direction; + + srand(time(NULL)); + + game = GetInit(); + SendInit(BOT_NAME); + + while (1) { + + GetFrame(game); + + for (x = 0 ; x < game.width ; x++) { + for (y = 0 ; y < game.height ; y++) { + if (game.owner[x][y] == game.playertag) { + direction = rand() % 5; + SetMove(game, x, y, direction); + } + } + } + + SendFrame(game); + } + + return 0; +} diff --git a/airesources/C/hlt.h b/airesources/C/hlt.h new file mode 100644 index 00000000..7cee6f85 --- /dev/null +++ b/airesources/C/hlt.h @@ -0,0 +1,277 @@ +/* + In general, none of the public functions return pointers, + nor do they take pointer arguments. + + Useful functions: + + GAME GetInit() + void SendInit(char *botname) + void GetFrame(GAME game) + void SetMove(GAME game, int x, int y, int direction) + void SendFrame(GAME game) + + Convenience functions: + + SITE GetSiteFromXY(GAME game, int x, int y) + SITE GetSiteFromMovement(GAME game, int src_x, int src_y, int direction) + + Pssst! More documentation and some better bots are at: + https://github.com/fohristiwhirl/chalite +*/ + +#include +#include + +#define STILL 0 +#define NORTH 1 +#define EAST 2 +#define SOUTH 3 +#define WEST 4 + +typedef struct Site_struct { + int x; + int y; + int owner; + int strength; + int production; +} SITE; + +typedef struct Game_struct { + int width; + int height; + int playertag; + int ** moves; + int ** owner; + int ** production; + int ** strength; +} GAME; + +int ** __new_2d_int_array(int width, int height) { + + int x; + int **result; + + result = malloc(sizeof(int*) * width); + if (result == NULL) { + printf("Malloc 1 failed in __new_2d_int_array()\n"); + exit(1); + } + + for (x = 0 ; x < width ; x++) { + result[x] = malloc(sizeof(int) * height); + if (result[x] == NULL) { + printf("Malloc 2 failed in __new_2d_int_array()\n"); + exit(1); + } + } + + return result; +} + +int __getnextint() { + + int ch; + + int result = 0; + int seen_any_digits = 0; + + while (1) { + ch = getchar(); + if (ch == EOF) { + printf("EOF received. Halite engine quit?\n"); + exit(1); + } + if (ch >= 48 && ch <= 57) { + seen_any_digits = 1; + result *= 10; + result += ch - 48; + } else { + if (seen_any_digits) { + return result; + } + } + } + + return 54321; // Never get here. +} + +void __parseproduction(GAME game) { + + int x, y; + + for (y = 0 ; y < game.height ; y++) { + for (x = 0 ; x < game.width ; x++) { + game.production[x][y] = __getnextint(); + } + } + return; +} + +void __parsemap(GAME game) { + + int x, y; + int run; + int owner; + int total_set; + int set_this_run; + + x = 0; + y = 0; + total_set = 0; + set_this_run = 0; + while (total_set < game.width * game.height) { + + run = __getnextint(); + owner = __getnextint(); + + for (set_this_run = 0 ; set_this_run < run ; set_this_run++) { + + game.owner[x][y] = owner; + total_set++; + + x++; + if (x == game.width) { + x = 0; + y += 1; + } + } + } + + for (y = 0 ; y < game.height ; y++) { + for (x = 0 ; x < game.width ; x++) { + game.strength[x][y] = __getnextint(); + } + } + + return; +} + +GAME GetInit() { + + GAME game; + + game.playertag = __getnextint(); + game.width = __getnextint(); + game.height = __getnextint(); + + game.moves = __new_2d_int_array(game.width, game.height); + game.owner = __new_2d_int_array(game.width, game.height); + game.production = __new_2d_int_array(game.width, game.height); + game.strength = __new_2d_int_array(game.width, game.height); + + __parseproduction(game); + __parsemap(game); + + return game; +} + +void SendInit(char *botname) { + printf("%s\n", botname); + fflush(stdout); +} + +void GetFrame(GAME game) { + + int x, y; + + __parsemap(game); + + // Reset the moves array while we're at it. + + for (x = 0 ; x < game.width ; x++) { + for (y = 0 ; y < game.height ; y++) { + game.moves[x][y] = STILL; + } + } + + return; +} + +int __sanitise_x(GAME game, int x) { + if (x < 0) { + x += -(x / game.width) * game.width + game.width; // Can make x == width, so must still use % next + } + x %= game.width; + return x; +} + +int __sanitise_y(GAME game, int y) { + if (y < 0) { + y += -(y / game.height) * game.height + game.height; // Can make y == height, so must still use % next + } + y %= game.height; + return y; +} + +SITE GetSiteFromXY(GAME game, int x, int y) { + + SITE result; + + x = __sanitise_x(game, x); + y = __sanitise_y(game, y); + + result.x = x; + result.y = y; + + result.owner = game.owner[result.x][result.y]; + result.production = game.production[result.x][result.y]; + result.strength = game.strength[result.x][result.y]; + + return result; +} + +SITE GetSiteFromMovement(GAME game, int src_x, int src_y, int direction) { + + SITE result; + int x, y; + + x = src_x; + y = src_y; + + switch (direction) { + case NORTH: + y--; + break; + case EAST: + x++; + break; + case SOUTH: + y++; + break; + case WEST: + x--; + break; + } + + x = __sanitise_x(game, x); + y = __sanitise_y(game, y); + + result = GetSiteFromXY(game, x, y); + + return result; +} + +void SetMove(GAME game, int x, int y, int direction) { + x = __sanitise_x(game, x); + y = __sanitise_y(game, y); + game.moves[x][y] = direction; + return; +} + +void SendFrame(GAME game) { + + int x, y; + + for (x = 0 ; x < game.width ; x++) { + for (y = 0 ; y < game.height ; y++) { + if (game.moves[x][y] != STILL && game.owner[x][y] == game.playertag) { + printf("%d %d %d ", x, y, game.moves[x][y]); + } + } + } + + printf("\n"); + fflush(stdout); + + return; +} diff --git a/airesources/C/runGame.bat b/airesources/C/runGame.bat new file mode 100644 index 00000000..9a48b1a9 --- /dev/null +++ b/airesources/C/runGame.bat @@ -0,0 +1,3 @@ +gcc MyBot.c -o MyBot.exe +gcc RandomBot.c -o RandomBot.exe +.\halite.exe -d "30 30" "MyBot.exe" "RandomBot.exe" diff --git a/airesources/C/runGame.sh b/airesources/C/runGame.sh new file mode 100755 index 00000000..c6e89366 --- /dev/null +++ b/airesources/C/runGame.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +gcc MyBot.c -o MyBot.o +gcc RandomBot.c -o RandomBot.o +./halite -d "30 30" "./MyBot.o" "./RandomBot.o" diff --git a/airesources/Clojure/runGame.bat b/airesources/Clojure/runGame.bat index fad5b273..54125f25 100644 --- a/airesources/Clojure/runGame.bat +++ b/airesources/Clojure/runGame.bat @@ -1,3 +1,3 @@ -lein uberjar +call lein uberjar -\halite.exe -d "30 30" "java -cp target/MyBot.jar MyBot" "java -cp target/MyBot.jar RandomBot" +halite.exe -d "30 30" "java -cp target/MyBot.jar MyBot" "java -cp target/MyBot.jar RandomBot" diff --git a/airesources/Go/MyBot.go b/airesources/Go/MyBot.go index dcf0cad3..e12f1b6b 100644 --- a/airesources/Go/MyBot.go +++ b/airesources/Go/MyBot.go @@ -6,19 +6,19 @@ import ( "math/rand" ) -func main () { - conn, gameMap := hlt.NewConnection("MyBot") +func main() { + conn, gameMap := hlt.NewConnection() + conn.SendName("MyBot") for { var moves hlt.MoveSet gameMap = conn.GetFrame() for y := 0; y < gameMap.Height; y++ { for x := 0; x < gameMap.Width; x++ { - loc := hlt.NewLocation(x,y) + loc := hlt.NewLocation(x, y) if gameMap.GetSite(loc, hlt.STILL).Owner == conn.PlayerTag { - moves = append(moves, hlt.Move { - Location: loc, + moves = append(moves, hlt.Move{ + Location: loc, Direction: hlt.Direction(rand.Int() % 5), - }) } } diff --git a/airesources/Go/RandomBot.go b/airesources/Go/RandomBot.go index d317c064..1e7ef8c5 100644 --- a/airesources/Go/RandomBot.go +++ b/airesources/Go/RandomBot.go @@ -6,19 +6,19 @@ import ( "math/rand" ) -func main () { - conn, gameMap := hlt.NewConnection("RandomBot") +func main() { + conn, gameMap := hlt.NewConnection() + conn.SendName("RandomBot") for { var moves hlt.MoveSet gameMap = conn.GetFrame() for y := 0; y < gameMap.Height; y++ { for x := 0; x < gameMap.Width; x++ { - loc := hlt.NewLocation(x,y) + loc := hlt.NewLocation(x, y) if gameMap.GetSite(loc, hlt.STILL).Owner == conn.PlayerTag { - moves = append(moves, hlt.Move { - Location: loc, + moves = append(moves, hlt.Move{ + Location: loc, Direction: hlt.Direction(rand.Int() % 5), - }) } } diff --git a/airesources/Go/src/hlt/networking.go b/airesources/Go/src/hlt/networking.go index 6e616e2a..03ce13aa 100644 --- a/airesources/Go/src/hlt/networking.go +++ b/airesources/Go/src/hlt/networking.go @@ -20,12 +20,11 @@ const ( WEST ) -var Directions = []Direction{STILL, NORTH,EAST, SOUTH, WEST} -var CARDINALS = []Direction{NORTH,EAST, SOUTH, WEST} - +var Directions = []Direction{STILL, NORTH, EAST, SOUTH, WEST} +var CARDINALS = []Direction{NORTH, EAST, SOUTH, WEST} type Site struct { - Owner int + Owner int Strength int Production int } @@ -58,10 +57,10 @@ func (ms MoveSet) serialize() string { type Connection struct { width, height int - PlayerTag int + PlayerTag int productions [][]int - reader *bufio.Reader - writer io.Writer + reader *bufio.Reader + writer io.Writer } func (c *Connection) deserializeMap() GameMap { @@ -130,7 +129,7 @@ func (c *Connection) deserializeProductions() { } } -func NewConnection(name string) (Connection, GameMap) { +func NewConnection() (Connection, GameMap) { conn := Connection{ reader: bufio.NewReader(os.Stdin), writer: os.Stdout, @@ -138,11 +137,13 @@ func NewConnection(name string) (Connection, GameMap) { conn.PlayerTag = conn.getInt() conn.deserializeMapSize() conn.deserializeProductions() - conn.sendString(name) - return conn, conn.deserializeMap() } +func (c *Connection) SendName(name string) { + c.sendString(name) +} + func (c *Connection) GetFrame() GameMap { return c.deserializeMap() } diff --git a/airesources/Java/Direction.java b/airesources/Java/Direction.java index ae2e9600..87a97e45 100644 --- a/airesources/Java/Direction.java +++ b/airesources/Java/Direction.java @@ -6,26 +6,8 @@ public enum Direction { public static final Direction[] DIRECTIONS = new Direction[]{STILL, NORTH, EAST, SOUTH, WEST}; public static final Direction[] CARDINALS = new Direction[]{NORTH, EAST, SOUTH, WEST}; - private static Direction fromInteger(int value) { - if(value == 0) { - return STILL; - } - if(value == 1) { - return NORTH; - } - if(value == 2) { - return EAST; - } - if(value == 3) { - return SOUTH; - } - if(value == 4) { - return WEST; - } - return null; - } - public static Direction randomDirection() { - return fromInteger(new Random().nextInt(5)); + Direction[] values = values(); + return values[new Random().nextInt(values.length)]; } } diff --git a/airesources/Java/GameMap.java b/airesources/Java/GameMap.java index 18f7e602..af24cb7a 100644 --- a/airesources/Java/GameMap.java +++ b/airesources/Java/GameMap.java @@ -1,24 +1,23 @@ import java.util.ArrayList; public class GameMap{ - public ArrayList< ArrayList > contents; - public int width, height; - public GameMap() { - width = 0; - height = 0; - contents = new ArrayList< ArrayList >(0); - } + private final Site[][] contents; + private final Location[][] locations; + public final int width, height; + + public GameMap(int width, int height, int[][] productions) { - public GameMap(int width_, int height_) { - width = width_; - height = height_; - contents = new ArrayList< ArrayList >(0); - for(int y = 0; y < height; y++) { - ArrayList row = new ArrayList(); + this.width = width; + this.height = height; + this.contents = new Site[width][height]; + this.locations = new Location[width][height]; + + for (int y = 0; y < height; y++) { for(int x = 0; x < width; x++) { - row.add(new Site()); + final Site site = new Site(productions[x][y]); + contents[x][y] = site; + locations[x][y] = new Location(x, y, site); } - contents.add(row); } } @@ -52,35 +51,42 @@ public double getAngle(Location loc1, Location loc2) { return Math.atan2(dy, dx); } - public Location getLocation(Location loc, Direction dir) { - Location l = new Location(loc); - if(dir != Direction.STILL) { - if(dir == Direction.NORTH) { - if(l.y == 0) l.y = height - 1; - else l.y--; - } - else if(dir == Direction.EAST) { - if(l.x == width - 1) l.x = 0; - else l.x++; - } - else if(dir == Direction.SOUTH) { - if(l.y == height - 1) l.y = 0; - else l.y++; - } - else if(dir == Direction.WEST) { - if(l.x == 0) l.x = width - 1; - else l.x--; - } + public Location getLocation(Location location, Direction direction) { + switch (direction) { + case STILL: + return location; + case NORTH: + return locations[location.getX()][(location.getY() == 0 ? height : location.getY()) -1]; + case EAST: + return locations[location.getX() == width - 1 ? 0 : location.getX() + 1][location.getY()]; + case SOUTH: + return locations[location.getX()][location.getY() == height - 1 ? 0 : location.getY() + 1]; + case WEST: + return locations[(location.getX() == 0 ? width : location.getX()) - 1][location.getY()]; + default: + throw new IllegalArgumentException(String.format("Unknown direction %s encountered", direction)); } - return l; } public Site getSite(Location loc, Direction dir) { - Location l = getLocation(loc, dir); - return contents.get(l.y).get(l.x); + return getLocation(loc, dir).getSite(); } public Site getSite(Location loc) { - return contents.get(loc.y).get(loc.x); + return loc.getSite(); + } + + public Location getLocation(int x, int y) { + return locations[x][y]; + } + + void reset() { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final Site site = contents[x][y]; + site.owner = 0; + site.strength = 0; + } + } } } diff --git a/airesources/Java/Location.java b/airesources/Java/Location.java index 122b4687..da8460fb 100644 --- a/airesources/Java/Location.java +++ b/airesources/Java/Location.java @@ -1,12 +1,24 @@ public class Location { - public int x, y; - public Location(int x_, int y_) { - x = x_; - y = y_; + // Public for backward compability + public final int x, y; + private final Site site; + + public Location(int x, int y, Site site) { + this.x = x; + this.y = y; + this.site = site; + } + + public int getX() { + return x; } - public Location(Location l) { - x = l.x; - y = l.y; + + public int getY() { + return y; + } + + public Site getSite() { + return site; } } diff --git a/airesources/Java/MyBot.java b/airesources/Java/MyBot.java index 64aeba49..e061bfc3 100644 --- a/airesources/Java/MyBot.java +++ b/airesources/Java/MyBot.java @@ -1,24 +1,26 @@ import java.util.ArrayList; +import java.util.List; public class MyBot { public static void main(String[] args) throws java.io.IOException { - InitPackage iPackage = Networking.getInit(); - int myID = iPackage.myID; - GameMap gameMap = iPackage.map; + + final InitPackage iPackage = Networking.getInit(); + final int myID = iPackage.myID; + final GameMap gameMap = iPackage.map; Networking.sendInit("MyJavaBot"); while(true) { - ArrayList moves = new ArrayList(); + List moves = new ArrayList(); - gameMap = Networking.getFrame(); + Networking.updateFrame(gameMap); - for(int y = 0; y < gameMap.height; y++) { - for(int x = 0; x < gameMap.width; x++) { - Site site = gameMap.getSite(new Location(x, y)); + for (int y = 0; y < gameMap.height; y++) { + for (int x = 0; x < gameMap.width; x++) { + final Location location = gameMap.getLocation(x, y); + final Site site = location.getSite(); if(site.owner == myID) { - Direction dir = Direction.randomDirection(); - moves.add(new Move(new Location(x, y), dir)); + moves.add(new Move(location, Direction.randomDirection())); } } } diff --git a/airesources/Java/Networking.java b/airesources/Java/Networking.java index 1fca0ef1..9f2c7b19 100644 --- a/airesources/Java/Networking.java +++ b/airesources/Java/Networking.java @@ -4,57 +4,57 @@ import java.util.Scanner; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.List; public class Networking { - public static final int SIZE_OF_INTEGER_PREFIX = 4; - public static final int CHAR_SIZE = 1; - private static int _width, _height; - private static ArrayList< ArrayList > _productions; - static void deserializeGameMapSize(String inputString) { - String[] inputStringComponents = inputString.split(" "); - - _width = Integer.parseInt(inputStringComponents[0]); - _height = Integer.parseInt(inputStringComponents[1]); - } - - - static void deserializeProductions(String inputString) { + static int[][] deserializeProductions(String inputString, int width, int height) { String[] inputStringComponents = inputString.split(" "); int index = 0; - _productions = new ArrayList< ArrayList >(); - for(int a = 0; a < _height; a++) { - ArrayList row = new ArrayList(); - for(int b = 0; b < _width; b++) { - row.add(Integer.parseInt(inputStringComponents[index])); + int[][] productions = new int[width][height]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + productions[x][y] = Integer.parseInt(inputStringComponents[index]); index++; } - _productions.add(row); } + + return productions; } - static String serializeMoveList(ArrayList moves) { + static String serializeMoveList(List moves) { StringBuilder builder = new StringBuilder(); - for(Move move : moves) builder.append(move.loc.x + " " + move.loc.y + " " + move.dir.ordinal() + " "); + for (Move move : moves) { + builder.append(move.loc.x) + .append(" ") + .append(move.loc.y) + .append(" ") + .append(move.dir.ordinal()) + .append(" "); + } return builder.toString(); } - static GameMap deserializeGameMap(String inputString) { + static GameMap deserializeGameMap(String inputString, GameMap map) { String[] inputStringComponents = inputString.split(" "); - GameMap map = new GameMap(_width, _height); - // Run-length encode of owners int y = 0, x = 0; int counter = 0, owner = 0; int currentIndex = 0; - while(y != map.height) { + + + while (y != map.height) { + counter = Integer.parseInt(inputStringComponents[currentIndex]); owner = Integer.parseInt(inputStringComponents[currentIndex + 1]); + currentIndex += 2; - for(int a = 0; a < counter; ++a) { - map.contents.get(y).get(x).owner = owner; + for (int a = 0; a < counter; a++) { + + map.getLocation(x,y).getSite().owner = owner; ++x; if(x == map.width) { x = 0; @@ -63,12 +63,11 @@ static GameMap deserializeGameMap(String inputString) { } } - for (int a = 0; a < map.contents.size(); ++a) { - for (int b = 0; b < map.contents.get(a).size(); ++b) { + for (int b = 0; b < map.height; b++) { + for (int a = 0; a < map.width; a++) { int strengthInt = Integer.parseInt(inputStringComponents[currentIndex]); currentIndex++; - map.contents.get(a).get(b).strength = strengthInt; - map.contents.get(a).get(b).production = _productions.get(a).get(b); + map.getLocation(a,b).getSite().strength = strengthInt; } } @@ -100,11 +99,22 @@ static String getString() { } static InitPackage getInit() { + InitPackage initPackage = new InitPackage(); initPackage.myID = (int)Integer.parseInt(getString()); - deserializeGameMapSize(getString()); - deserializeProductions(getString()); - initPackage.map = deserializeGameMap(getString()); + + // Deserialize width and height: + final String[] inputStringComponents = getString().split(" "); + + int width = Integer.parseInt(inputStringComponents[0]); + int height = Integer.parseInt(inputStringComponents[1]); + + int[][] productions = deserializeProductions(getString(), width, height); + + GameMap map = new GameMap(width, height, productions); + deserializeGameMap(getString(), map); + + initPackage.map = map; return initPackage; } @@ -113,11 +123,12 @@ static void sendInit(String name) { sendString(name); } - static GameMap getFrame() { - return deserializeGameMap(getString()); + static void updateFrame(GameMap map) { + map.reset(); + deserializeGameMap(getString(), map); } - static void sendFrame(ArrayList moves) { + static void sendFrame(List moves) { sendString(serializeMoveList(moves)); } diff --git a/airesources/Java/RandomBot.java b/airesources/Java/RandomBot.java index 2e66cd99..993de20f 100644 --- a/airesources/Java/RandomBot.java +++ b/airesources/Java/RandomBot.java @@ -1,28 +1,30 @@ import java.util.ArrayList; +import java.util.List; public class RandomBot { public static void main(String[] args) throws java.io.IOException { - InitPackage iPackage = Networking.getInit(); - int myID = iPackage.myID; - GameMap gameMap = iPackage.map; + + final InitPackage iPackage = Networking.getInit(); + final int myID = iPackage.myID; + final GameMap gameMap = iPackage.map; Networking.sendInit("RandomJavaBot"); while(true) { - ArrayList moves = new ArrayList(); + List moves = new ArrayList(); - gameMap = Networking.getFrame(); + Networking.updateFrame(gameMap); - for(int y = 0; y < gameMap.height; y++) { - for(int x = 0; x < gameMap.width; x++) { - Site site = gameMap.getSite(new Location(x, y)); + for (int y = 0; y < gameMap.height; y++) { + for (int x = 0; x < gameMap.width; x++) { + final Location location = gameMap.getLocation(x, y); + final Site site = location.getSite(); if(site.owner == myID) { - Direction dir = Direction.randomDirection(); - moves.add(new Move(new Location(x, y), dir)); + moves.add(new Move(location, Direction.randomDirection())); } } } - Networking.sendFrame(moves); + Networking.sendFrame(moves); } } } diff --git a/airesources/Java/Site.java b/airesources/Java/Site.java index 7f8c3e41..a8302913 100644 --- a/airesources/Java/Site.java +++ b/airesources/Java/Site.java @@ -1,3 +1,9 @@ public class Site { - public int owner, strength, production; + + public final int production; + public int owner, strength; + + public Site(int production) { + this.production = production; + } } diff --git a/airesources/Julia/MyBot.jl b/airesources/Julia/MyBot.jl new file mode 100644 index 00000000..cc26f5f7 --- /dev/null +++ b/airesources/Julia/MyBot.jl @@ -0,0 +1,18 @@ +include("hlt.jl") +include("networking.jl") + +myID, gameMap = getInit() +sendInit("MyJuliaBot") + +while true + moves = Vector{Move}() + gameMap = getFrame() + for y in 0:gameMap.height-1 + for x in 0:gameMap.width-1 + if getSite(gameMap, Location(x, y)).owner == myID + push!(moves, Move(Location(x, y), rand(0:4))) + end + end + end + sendFrame(moves) +end diff --git a/airesources/Julia/RandomBot.jl b/airesources/Julia/RandomBot.jl new file mode 100644 index 00000000..426316e2 --- /dev/null +++ b/airesources/Julia/RandomBot.jl @@ -0,0 +1,18 @@ +include("hlt.jl") +include("networking.jl") + +myID, gameMap = getInit() +sendInit("RandomJuliaBot") + +while true + moves = Vector{Move}() + gameMap = getFrame() + for y in 0:gameMap.height-1 + for x in 0:gameMap.width-1 + if getSite(gameMap, Location(x, y)).owner == myID + push!(moves, Move(Location(x, y), rand(0:4))) + end + end + end + sendFrame(moves) +end diff --git a/airesources/Julia/hlt.jl b/airesources/Julia/hlt.jl new file mode 100644 index 00000000..30bafa7c --- /dev/null +++ b/airesources/Julia/hlt.jl @@ -0,0 +1,124 @@ +const STILL = 0 +const NORTH = 1 +const EAST = 2 +const SOUTH = 3 +const WEST = 4 + +const DIRECTIONS = [a for a in 0:4] +const CARDINALS = [a for a in 1:4] + +const ATTACK = 0 +const STOP_ATTACK = 1 + +type Location + x :: Int64 + y :: Int64 + Location(x::Int64, y::Int64) = new(x, y) + Location() = Location(0, 0) +end + +type Site + owner :: Int64 + strength :: Int64 + production :: Int64 + Site(owner::Int64, strength::Int64, production::Int64) = new(owner, strength, production) + Site() = Site(0, 0, 0) +end + +type Move + loc :: Location + direction :: Int64 + Move(loc::Location, direction::Int64) = new(loc, direction) + Move() = Move(Location(), 0) +end + +type GameMap + width :: Int64 + height :: Int64 + contents :: Vector{Vector{Site}} + function GameMap(width::Int64, height::Int64) + contents = Vector{Vector{Site}}() + for y in 0:height-1 + row = Vector{Site}() + for x in 0:width-1 + push!(row, Site(0, 0, 0)) + end + push!(contents, row) + end + new(width, height, contents) + end + GameMap() = GameMap(0, 0) +end + +isBounds(gm::GameMap, l::Location) = l.x >= 0 && l.x < gm.width && l.y >= 0 && l.y < gm.height + +function getDistance(gm::GameMap, l1::Location, l2::Location) + dx = abs(l1.x - l2.x) + dy = abs(l1.y - l2.y) + + if dx > gm.width / 2 + dx = gm.width - dx + end + + if dy > gm.height / 2 + dy = gm.height - dy + end + + dx + dy +end + +function getAngle(gm::GameMap, l1::Location, l2::Location) + dx = l2.x - l1.x + dy = l2.y - l1.y + + if dx > gm.width - dx + dx -= gm.width + elseif -dx > gm.width + dx + dx += gm.width + end + + if dy > gm.height - dy + dy -= gm.height + elseif -dy > gm.height + dy + dy += gm.height + end + + atan2(dy, dx) +end + +function getLocation(gm::GameMap, loc::Location, direction::Int64) + l = deepcopy(loc) + if direction != STILL + if direction == NORTH + if l.y == 0 + l.y = gm.height - 1 + else + l.y = l.y - 1 + end + elseif direction == EAST + if l.x == gm.width - 1 + l.x = 0 + else + l.x = l.x + 1 + end + elseif direction == SOUTH + if l.y == gm.height - 1 + l.y = 0 + else + l.y = l.y + 1 + end + elseif direction == WEST + if l.x == 0 + l.x = gm.width - 1 + else + l.x = l.x - 1 + end + end + end + l +end + +function getSite(gm::GameMap, l::Location, direction::Int64=STILL) + l = getLocation(gm, l, direction) + gm.contents[l.y + 1][l.x + 1] +end diff --git a/airesources/Julia/networking.jl b/airesources/Julia/networking.jl new file mode 100644 index 00000000..72566fc1 --- /dev/null +++ b/airesources/Julia/networking.jl @@ -0,0 +1,95 @@ +_productions = Vector{Vector{Int64}}() +_width = nothing +_height = nothing + +function serializeMoveSet(moves::Vector{Move}) + returnString = "" + for move in moves + returnString *= string(move.loc.x) * " " * string(move.loc.y) * " " * string(move.direction) * " " + end + returnString +end + +function deserializeMapSize(inputString::String) + splitString = reverse(split(inputString, " ")) + + global _width, _height + _width = parse(pop!(splitString)) + _height = parse(pop!(splitString)) +end + +function deserializeProductions(inputString::String) + splitString = reverse(split(inputString, " ")) + + for a in 1:_height + row = Vector{Int64}() + for b in 1:_width + push!(row, parse(pop!(splitString))) + end + push!(_productions, row) + end +end + +function deserializeMap(inputString::String) + splitString = reverse(split(inputString, " ")) + + m = GameMap(_width, _height) + + y = 1 + x = 1 + counter = 0 + owner = 0 + while ~(y > m.height) + counter = parse(pop!(splitString)) + owner = parse(pop!(splitString)) + for a in 0:counter-1 + m.contents[y][x].owner = owner + x += 1 + if x > m.width + x = 1 + y += 1 + end + end + end + + for a in 1:_height + for b in 1:_width + m.contents[a][b].strength = parse(pop!(splitString)) + m.contents[a][b].production = _productions[a][b] + end + end + + m +end + +function sendString(toBeSent::String) + toBeSent *= "\n" + + write(STDOUT, toBeSent) + flush(STDOUT) +end + +function getString() + rstrip(readline(), '\n') +end + +function getInit() + playerTag = parse(getString()) + deserializeMapSize(getString()) + deserializeProductions(getString()) + m = deserializeMap(getString()) + + (playerTag, m) +end + +function sendInit(name::String) + sendString(name) +end + +function getFrame() + return deserializeMap(getString()) +end + +function sendFrame(moves::Vector{Move}) + sendString(serializeMoveSet(moves)) +end diff --git a/airesources/Julia/runGame.bat b/airesources/Julia/runGame.bat new file mode 100644 index 00000000..a8a74542 --- /dev/null +++ b/airesources/Julia/runGame.bat @@ -0,0 +1 @@ +.\halite.exe -d "30 30" "julia MyBot.jl" "julia RandomBot.jl" diff --git a/airesources/Julia/runGame.sh b/airesources/Julia/runGame.sh new file mode 100755 index 00000000..495639cc --- /dev/null +++ b/airesources/Julia/runGame.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./halite -d "30 30" "julia MyBot.jl" "julia RandomBot.jl" diff --git a/airesources/Python/MyBot.py b/airesources/Python/MyBot.py index a270f399..83501506 100644 --- a/airesources/Python/MyBot.py +++ b/airesources/Python/MyBot.py @@ -1,15 +1,12 @@ -from hlt import * -from networking import * +import hlt +from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square +import random -myID, gameMap = getInit() -sendInit("MyPythonBot") + +myID, game_map = hlt.get_init() +hlt.send_init("MyPythonBot") while True: - moves = [] - gameMap = getFrame() - for y in range(gameMap.height): - for x in range(gameMap.width): - location = Location(x, y) - if gameMap.getSite(location).owner == myID: - moves.append(Move(location, random.choice(DIRECTIONS))) - sendFrame(moves) + game_map.get_frame() + moves = [Move(square, random.choice((NORTH, EAST, SOUTH, WEST, STILL))) for square in game_map if square.owner == myID] + hlt.send_frame(moves) diff --git a/airesources/Python/RandomBot.py b/airesources/Python/RandomBot.py index e7f7949a..0a6c8a28 100644 --- a/airesources/Python/RandomBot.py +++ b/airesources/Python/RandomBot.py @@ -1,15 +1,12 @@ -from hlt import * -from networking import * +import hlt +from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square +import random -myID, gameMap = getInit() -sendInit("RandomPythonBot") + +myID, game_map = hlt.get_init() +hlt.send_init("RandomPythonBot") while True: - moves = [] - gameMap = getFrame() - for y in range(gameMap.height): - for x in range(gameMap.width): - location = Location(x, y) - if gameMap.getSite(location).owner == myID: - moves.append(Move(location, random.choice(DIRECTIONS))) - sendFrame(moves) + game_map.get_frame() + moves = [Move(square, random.choice((NORTH, EAST, SOUTH, WEST, STILL))) for square in game_map if square.owner == myID] + hlt.send_frame(moves) diff --git a/airesources/Python/hlt.py b/airesources/Python/hlt.py index 3e3e992e..cc8cceae 100644 --- a/airesources/Python/hlt.py +++ b/airesources/Python/hlt.py @@ -1,96 +1,111 @@ -import random -import math -import copy - -STILL = 0 -NORTH = 1 -EAST = 2 -SOUTH = 3 -WEST = 4 - -DIRECTIONS = [a for a in range(0, 5)] -CARDINALS = [a for a in range(1, 5)] - -ATTACK = 0 -STOP_ATTACK = 1 - -class Location: - def __init__(self, x=0, y=0): - self.x = x - self.y = y -class Site: - def __init__(self, owner=0, strength=0, production=0): - self.owner = owner - self.strength = strength - self.production = production -class Move: - def __init__(self, loc=0, direction=0): - self.loc = loc - self.direction = direction +import sys +from collections import namedtuple +from itertools import chain, zip_longest + + +def grouper(iterable, n, fillvalue=None): + "Collect data into fixed-length chunks or blocks" + # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return zip_longest(*args, fillvalue=fillvalue) + + +NORTH, EAST, SOUTH, WEST, STILL = range(5) + + +def opposite_cardinal(direction): + "Returns the opposing cardinal direction." + return (direction + 2) % 4 if direction != STILL else STILL + + +Square = namedtuple('Square', 'x y owner strength production') + + +Move = namedtuple('Move', 'square direction') + class GameMap: - def __init__(self, width = 0, height = 0, numberOfPlayers = 0): - self.width = width - self.height = height - self.contents = [] - - for y in range(0, self.height): - row = [] - for x in range(0, self.width): - row.append(Site(0, 0, 0)) - self.contents.append(row) - - def inBounds(self, l): - return l.x >= 0 and l.x < self.width and l.y >= 0 and l.y < self.height - - def getDistance(self, l1, l2): - dx = abs(l1.x - l2.x) - dy = abs(l1.y - l2.y) - if dx > self.width / 2: - dx = self.width - dx - if dy > self.height / 2: - dy = self.height - dy + def __init__(self, size_string, production_string, map_string=None): + self.width, self.height = tuple(map(int, size_string.split())) + self.production = tuple(tuple(map(int, substring)) for substring in grouper(production_string.split(), self.width)) + self.contents = None + self.get_frame(map_string) + self.starting_player_count = len(set(square.owner for square in self)) - 1 + + def get_frame(self, map_string=None): + "Updates the map information from the latest frame provided by the Halite game environment." + if map_string is None: + map_string = get_string() + split_string = map_string.split() + owners = list() + while len(owners) < self.width * self.height: + counter = int(split_string.pop(0)) + owner = int(split_string.pop(0)) + owners.extend([owner] * counter) + assert len(owners) == self.width * self.height + assert len(split_string) == self.width * self.height + self.contents = [[Square(x, y, owner, strength, production) + for x, (owner, strength, production) + in enumerate(zip(owner_row, strength_row, production_row))] + for y, (owner_row, strength_row, production_row) + in enumerate(zip(grouper(owners, self.width), + grouper(map(int, split_string), self.width), + self.production))] + + def __iter__(self): + "Allows direct iteration over all squares in the GameMap instance." + return chain.from_iterable(self.contents) + + def neighbors(self, square, n=1, include_self=False): + "Iterable over the n-distance neighbors of a given square. For single-step neighbors, the enumeration index provides the direction associated with the neighbor." + assert isinstance(include_self, bool) + assert isinstance(n, int) and n > 0 + if n == 1: + combos = ((0, -1), (1, 0), (0, 1), (-1, 0), (0, 0)) # NORTH, EAST, SOUTH, WEST, STILL ... matches indices provided by enumerate(game_map.neighbors(square)) + else: + combos = ((dx, dy) for dy in range(-n, n+1) for dx in range(-n, n+1) if abs(dx) + abs(dy) <= n) + return (self.contents[(square.y + dy) % self.height][(square.x + dx) % self.width] for dx, dy in combos if include_self or dx or dy) + + def get_target(self, square, direction): + "Returns a single, one-step neighbor in a given direction." + dx, dy = ((0, -1), (1, 0), (0, 1), (-1, 0), (0, 0))[direction] + return self.contents[(square.y + dy) % self.height][(square.x + dx) % self.width] + + def get_distance(self, sq1, sq2): + "Returns Manhattan distance between two squares." + dx = min(abs(sq1.x - sq2.x), sq1.x + self.width - sq2.x, sq2.x + self.width - sq1.x) + dy = min(abs(sq1.y - sq2.y), sq1.y + self.height - sq2.y, sq2.y + self.height - sq1.y) return dx + dy - def getAngle(self, l1, l2): - dx = l2.x - l1.x - dy = l2.y - l1.y - - if dx > self.width - dx: - dx -= self.width - elif -dx > self.width + dx: - dx += self.width - - if dy > self.height - dy: - dy -= self.height - elif -dy > self.height + dy: - dy += self.height - return math.atan2(dy, dx) - - def getLocation(self, loc, direction): - l = copy.deepcopy(loc) - if direction != STILL: - if direction == NORTH: - if l.y == 0: - l.y = self.height - 1 - else: - l.y -= 1 - elif direction == EAST: - if l.x == self.width - 1: - l.x = 0 - else: - l.x += 1 - elif direction == SOUTH: - if l.y == self.height - 1: - l.y = 0 - else: - l.y += 1 - elif direction == WEST: - if l.x == 0: - l.x = self.width - 1 - else: - l.x -= 1 - return l - def getSite(self, l, direction = STILL): - l = self.getLocation(l, direction) - return self.contents[l.y][l.x] +################################################################# +# Functions for communicating with the Halite game environment # +################################################################# + + +def send_string(s): + sys.stdout.write(s) + sys.stdout.write('\n') + sys.stdout.flush() + + +def get_string(): + return sys.stdin.readline().rstrip('\n') + + +def get_init(): + playerID = int(get_string()) + m = GameMap(get_string(), get_string()) + return playerID, m + + +def send_init(name): + send_string(name) + + +def translate_cardinal(direction): + "Translate direction constants used by this Python-based bot framework to that used by the official Halite game environment." + return (direction + 1) % 5 + + +def send_frame(moves): + send_string(' '.join(str(move.square.x) + ' ' + str(move.square.y) + ' ' + str(translate_cardinal(move.direction)) for move in moves)) diff --git a/airesources/Python/networking.py b/airesources/Python/networking.py deleted file mode 100644 index df189b40..00000000 --- a/airesources/Python/networking.py +++ /dev/null @@ -1,84 +0,0 @@ -from hlt import * -import socket -import traceback -import struct -from ctypes import * -import sys - -_productions = [] -_width = -1 -_height = -1 - -def serializeMoveSet(moves): - returnString = "" - for move in moves: - returnString += str(move.loc.x) + " " + str(move.loc.y) + " " + str(move.direction) + " " - return returnString - -def deserializeMapSize(inputString): - splitString = inputString.split(" ") - - global _width, _height - _width = int(splitString.pop(0)) - _height = int(splitString.pop(0)) - -def deserializeProductions(inputString): - splitString = inputString.split(" ") - - for a in range(0, _height): - row = [] - for b in range(0, _width): - row.append(int(splitString.pop(0))) - _productions.append(row) - -def deserializeMap(inputString): - splitString = inputString.split(" ") - - m = GameMap(_width, _height) - - y = 0 - x = 0 - counter = 0 - owner = 0 - while y != m.height: - counter = int(splitString.pop(0)) - owner = int(splitString.pop(0)) - for a in range(0, counter): - m.contents[y][x].owner = owner - x += 1 - if x == m.width: - x = 0 - y += 1 - - for a in range(0, _height): - for b in range(0, _width): - m.contents[a][b].strength = int(splitString.pop(0)) - m.contents[a][b].production = _productions[a][b] - - return m - -def sendString(toBeSent): - toBeSent += '\n' - - sys.stdout.write(toBeSent) - sys.stdout.flush() - -def getString(): - return sys.stdin.readline().rstrip('\n') - -def getInit(): - playerTag = int(getString()) - deserializeMapSize(getString()) - deserializeProductions(getString()) - m = deserializeMap(getString()) - - return (playerTag, m) - -def sendInit(name): - sendString(name) - -def getFrame(): - return deserializeMap(getString()) - -def sendFrame(moves): - sendString(serializeMoveSet(moves)) diff --git a/airesources/Scala/Bot.scala b/airesources/Scala/Bot.scala new file mode 100644 index 00000000..f289a4c5 --- /dev/null +++ b/airesources/Scala/Bot.scala @@ -0,0 +1,8 @@ +trait Bot { + def getMoves(grid: Grid): Iterable[Move] +} + +trait BotFactory { + def make(id: Int): Bot +} + diff --git a/airesources/Scala/Direction.scala b/airesources/Scala/Direction.scala new file mode 100644 index 00000000..4aebb157 --- /dev/null +++ b/airesources/Scala/Direction.scala @@ -0,0 +1,20 @@ +import scala.util.Random + +object Direction { + private val random = new Random() + val CARDINALS = Seq(North, East, South, West) + val ALL = Seq(Still, North, East, South, West) + + def getRandomDir: Direction = ALL(random.nextInt(5)) + def getRandomCard: Direction = CARDINALS(random.nextInt(4)) +} + +class Direction(value: Int) { + def getValue = value +} + +case object Still extends Direction(0) +case object North extends Direction(1) +case object East extends Direction(2) +case object South extends Direction(3) +case object West extends Direction(4) diff --git a/airesources/Scala/Env.scala b/airesources/Scala/Env.scala new file mode 100644 index 00000000..2dd667f1 --- /dev/null +++ b/airesources/Scala/Env.scala @@ -0,0 +1,84 @@ +object Env { + def readId(): Int = { + read().toInt + } + + def readInit(): Grid = { + val dims = read().split(" ") + val width = dims(0).toInt + val height = dims(1).toInt + + val locations = Array.ofDim[Location](height, width) + val productions = read().split(" ") + + for (y <- 0 until height) { + for (x <- 0 until width) { + locations(y)(x) = Location(x, y, productions(y * width + x).toInt) + } + } + + val occupants = readFrame(width, height) + new Grid(width, height, locations, occupants) + } + + def readFrame(width: Int, height: Int): Array[Array[Occupant]] = { + val ownersStrengths = read().split(" ") + val strengthsIndex = ownersStrengths.length - width * height + + var y, x, count = 0 + var owner, cur, i = 0 + + val occupants = Array.ofDim[Occupant](height, width) + while (y < height) { + if (cur < count) { + occupants(y)(x) = Occupant(owner, ownersStrengths(strengthsIndex + y * width + x).toInt) + if (x == width - 1) { + x = 0 + y += 1 + } else { + x += 1 + } + cur += 1 + } else { + count = ownersStrengths(i).toInt + owner = ownersStrengths(i + 1).toInt + cur = 0 + i += 2 + } + } + + occupants + } + + def writeFrame(moves: Iterable[Move]): Unit = { + val builder = new StringBuilder() + moves foreach { m => + builder.append(m.location.x) + builder.append(" ") + builder.append(m.location.y) + builder.append(" ") + builder.append(m.direction.getValue) + builder.append(" ") + } + writeString(builder.toString()) + } + + def writeInit(name: String): Unit = { + writeString(name) + } + + private def writeString(s: String): Unit = { + System.out.print(s + '\n') + System.out.flush() + } + + private def read(): String = { + val buffer = new StringBuilder() + var next = System.in.read() + while (next != '\n' && next != 0) { + buffer.append(next.asInstanceOf[Char]) + next = System.in.read() + } + buffer.toString + } +} diff --git a/airesources/Scala/Grid.scala b/airesources/Scala/Grid.scala new file mode 100644 index 00000000..a4ce9bf5 --- /dev/null +++ b/airesources/Scala/Grid.scala @@ -0,0 +1,93 @@ +object Grid { + val NEUTRAL = 0 +} + +class Grid(width: Int, height: Int, locations: Array[Array[Location]], var occupants: Array[Array[Occupant]]) { + def update(occupants: Array[Array[Occupant]]) = this.occupants = occupants + + def getWidth = width + def getHeight = height + + def getOccupant(x: Int, y: Int): Occupant = occupants(y)(x) + def getLocation(x: Int, y: Int): Location = locations(y)(x) + def getSite(x: Int, y: Int): Site = Site(getLocation(x, y), getOccupant(x, y)) + + def getSites: Seq[Site] = { + for { + x <- 0 until width + y <- 0 until height + } yield getSite(x, y) + } + + def getMine(id: Int): Seq[Site] = { + for { + x <- 0 until width + y <- 0 until height + if getOccupant(x, y).id == id + } yield getSite(x, y) + } + + def getDistance(first: Location, second: Location): Int = { + var dx = Math.abs(first.x - second.x) + var dy = Math.abs(first.y - second.y) + + if (dx > width / 2.0) { + dx = width - dx + } + + if (dy > height / 2.0) { + dy = height - dy + } + + dx + dy + } + + def getAngle(first: Location, second: Location): Double = { + var dx = first.x - second.x + + // Flip order because 0,0 is top left + // and want atan2 to look as it would on the unit circle + var dy = second.y - first.y + + if (dx > width - dx) { + dx -= width + } + if (-dx > width + dx) { + dx += width + } + + if (dy > height - dy) { + dy -= height + } + if (-dy > height + dy) { + dy += height + } + + Math.atan2(dy, dx) + } + + def getNeighbor(location: Location, direction: Direction): Neighbor = { + val x = location.x + val y = location.y + direction match { + case North => Neighbor(getSite(x, if (y == 0) height - 1 else y - 1), North) + case East => Neighbor(getSite(if (x == width - 1) 0 else x + 1, y), East) + case South => Neighbor(getSite(x, if (y == height - 1) 0 else y + 1), South) + case West => Neighbor(getSite(if (x == 0) width - 1 else x - 1, y), West) + case Still => Neighbor(getSite(x, y), Still) + } + } + + def getNeighbors(location: Location): Seq[Neighbor] = { + Direction.CARDINALS map (d => getNeighbor(location, d)) + } + + def getMyNeighbors(id: Int, location: Location): Seq[Neighbor] = { + getNeighbors(location).filter(_.site.occupant.id == id) + } +} + +case class Location(x: Int, y: Int, production: Int) +case class Occupant(id: Int, strength: Int) +case class Site(location: Location, occupant: Occupant) +case class Neighbor(site: Site, direction: Direction) diff --git a/airesources/Scala/HaliteBot.scala b/airesources/Scala/HaliteBot.scala deleted file mode 100644 index 4285d3a2..00000000 --- a/airesources/Scala/HaliteBot.scala +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Created by snoe on 7/23/16. - */ - -class MoveList extends java.util.ArrayList[Move] - -class HaliteBot(id: Int, gameMap: GameMap) { - - def takeTurn(turn:BigInt, gameMap:GameMap): MoveList = new MoveList() - - def name = "ScalaBot" -} - -trait HaliteBotMaker { - def makeBot(id:Int, gameMap:GameMap):HaliteBot = new HaliteBot(id, gameMap) -} - -object HaliteBot { - - /* - * A helper to call the bot from the infinite stream of turns - */ - private def runTurn(bot:HaliteBot)(turn:BigInt):Boolean = { - val gameMap = Networking.getFrame - val moves = bot.takeTurn(turn, gameMap) - Networking.sendFrame(moves) - turn >= 0 - } - - /* - * Run loop for the bot - */ - def run(args: Array[String], botMaker:HaliteBotMaker) = { -// val turns = Stream.iterate[BigInt](0){n => n + 1} - val iPackage: InitPackage = Networking.getInit(); - val bot: HaliteBot = botMaker.makeBot(iPackage.myID, iPackage.map) - Networking.sendInit(bot.name) - def runner = runTurn(bot)(_) - for (i <- 1 to 10000) { - runTurn(bot)(i) - } -// turns.takeWhile(runner) - } - -} diff --git a/airesources/Scala/Move.scala b/airesources/Scala/Move.scala new file mode 100644 index 00000000..cadd47fa --- /dev/null +++ b/airesources/Scala/Move.scala @@ -0,0 +1 @@ +case class Move(location: Location, direction: Direction) \ No newline at end of file diff --git a/airesources/Scala/MyBot.scala b/airesources/Scala/MyBot.scala index 7a0ec207..2090634f 100644 --- a/airesources/Scala/MyBot.scala +++ b/airesources/Scala/MyBot.scala @@ -1,33 +1,15 @@ -/** - * Created by snoe on 7/23/16. - */ -class MyBot(id: Int, gameMap:GameMap) extends HaliteBot(id, gameMap) { - - override def takeTurn(turn:BigInt, gameMap:GameMap): MoveList = { - // Random moves - val moves = new MoveList() - for (y <- 0 to gameMap.height - 1) { - for (x <- 0 to gameMap.width - 1) { - val site: Site = gameMap.getSite(new Location(x, y)) - if (site.owner == id) { - val dir: Direction = Direction.randomDirection - moves.add(new Move(new Location(x, y), dir)) - } - } - } - moves +object MyBot extends BotFactory { + def main(args: Array[String]): Unit = { + Runner.run("scalaMyBot", this) } + override def make(id: Int): Bot = new MyBot(id) } -object MyBot { - - def main(args:Array[String]):Unit = { - - val maker = new HaliteBotMaker() { - override def makeBot(id:Int, gameMap:GameMap):HaliteBot = new MyBot(id, gameMap) - } - - HaliteBot.run(args, maker) +class MyBot(myId: Int) extends Bot { + override def getMoves(grid: Grid): Iterable[Move] = { + for { + site <- grid.getMine(myId) + } yield Move(site.location, Direction.getRandomDir) } -} +} \ No newline at end of file diff --git a/airesources/Scala/RandomBot.scala b/airesources/Scala/RandomBot.scala index 2c6fb49e..5d1eea1c 100644 --- a/airesources/Scala/RandomBot.scala +++ b/airesources/Scala/RandomBot.scala @@ -1,33 +1,15 @@ -/** - * Created by snoe on 7/23/16. - */ -class RandomBot(id: Int, gameMap:GameMap) extends HaliteBot(id, gameMap) { - - override def takeTurn(turn:BigInt, gameMap:GameMap): MoveList = { - // Random moves - val moves = new MoveList() - for (y <- 0 to gameMap.height - 1) { - for (x <- 0 to gameMap.width - 1) { - val site: Site = gameMap.getSite(new Location(x, y)) - if (site.owner == id) { - val dir: Direction = Direction.randomDirection - moves.add(new Move(new Location(x, y), dir)) - } - } - } - moves +object RandomBot extends BotFactory { + def main(args: Array[String]): Unit = { + Runner.run("scalaRandom", this) } + override def make(id: Int): Bot = new RandomBot(id) } -object RandomBot { - - def main(args:Array[String]):Unit = { - - val maker = new HaliteBotMaker() { - override def makeBot(id:Int, gameMap:GameMap):HaliteBot = new RandomBot(id, gameMap) - } - - HaliteBot.run(args, maker) +class RandomBot(myId: Int) extends Bot { + override def getMoves(grid: Grid): Iterable[Move] = { + for { + site <- grid.getMine(myId) + } yield Move(site.location, Direction.getRandomDir) } -} +} \ No newline at end of file diff --git a/airesources/Scala/Runner.scala b/airesources/Scala/Runner.scala new file mode 100644 index 00000000..8509f058 --- /dev/null +++ b/airesources/Scala/Runner.scala @@ -0,0 +1,16 @@ +object Runner { + def run(name: String, botFactory: BotFactory): Unit = { + val id = Env.readId() + val grid = Env.readInit() + + val bot = botFactory.make(id) + Env.writeInit(name) + + while (true) { + val occupants = Env.readFrame(grid.getWidth, grid.getHeight) + grid.update(occupants) + val moves = bot.getMoves(grid) + Env.writeFrame(moves) + } + } +} diff --git a/airesources/Scala/runGame.sh b/airesources/Scala/runGame.sh index 906e2de5..1c7b49ab 100755 --- a/airesources/Scala/runGame.sh +++ b/airesources/Scala/runGame.sh @@ -1,8 +1,5 @@ #!/bin/bash -javac *.java -scalac HaliteBot.scala -scalac MyBot.scala -scalac RandomBot.scala +scalac *.scala ./halite -d "30 30" "scala MyBot" "scala RandomBot" diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..24367a16 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,28 @@ +# Specify version format +version: "{build}" + +only_commits: + message: /build/ + +# Build worker image (VM template) +image: Windows Server 2012 R2 + +# clone directory +clone_folder: C:\Halite + +# branches to build +branches: + only: + - master + +#before_build: +# - dir \s + +# scripts that run after cloning repository +build_script: + - cd C:\Halite\environment + - .\make.bat + +artifacts: + - path: environment\halite.exe + name: halite.exe diff --git a/environment/core/Halite.cpp b/environment/core/Halite.cpp index 8611f6da..861fa7c4 100644 --- a/environment/core/Halite.cpp +++ b/environment/core/Halite.cpp @@ -204,12 +204,18 @@ std::vector Halite::processNextFrame(std::vector alive) { } //Public Functions ------------------- -Halite::Halite(unsigned short width_, unsigned short height_, unsigned int seed_, Networking networking_, bool shouldIgnoreTimeout) { +Halite::Halite(unsigned short width_, unsigned short height_, unsigned int seed_, unsigned short n_players_for_map_creation, Networking networking_, bool shouldIgnoreTimeout) { networking = networking_; + // number_of_players is the number of active bots to start the match; it is constant throughout game number_of_players = networking.numberOfPlayers(); //Initialize map - game_map = hlt::Map(width_, height_, number_of_players, seed_); + game_map = hlt::Map(width_, height_, n_players_for_map_creation, seed_); + + //If this is single-player mode, remove all the extra players (they were automatically inserted in map, just 0 them out) + if (number_of_players == 1){ + for(unsigned short b = 0; b < game_map.map_height; b++) for(unsigned short c = 0; c < game_map.map_width; c++) if(game_map.contents[b][c].owner > 1) game_map.contents[b][c].owner = 0; + } //Default initialize player_moves = std::vector< std::map >(); @@ -226,6 +232,7 @@ Halite::Halite(unsigned short width_, unsigned short height_, unsigned int seed_ ignore_timeout = shouldIgnoreTimeout; //Init statistics + productive_squares_remaining = 1; // just more than zero to get through the game_loop the first time alive_frame_count = std::vector(number_of_players, 1); last_territory_count = std::vector(number_of_players, 1); full_territory_count = std::vector(number_of_players, 1); @@ -299,7 +306,7 @@ void Halite::output(std::string filename) { gameFile.close(); } -GameStatistics Halite::runGame(std::vector * names_, unsigned int seed, unsigned int id) { +GameStatistics Halite::runGame(std::vector * names_, unsigned int seed, unsigned int id, bool enabledReplay, std::string replayDirectory) { //For rankings std::vector result(number_of_players, true); std::vector rankings; @@ -325,7 +332,7 @@ GameStatistics Halite::runGame(std::vector * names_, unsigned int s for(auto a = names_->begin(); a != names_->end(); a++) player_names.push_back(a->substr(0, 30)); } const int maxTurnNumber = sqrt(game_map.map_width * game_map.map_height) * 10; - while(std::count(result.begin(), result.end(), true) > 1 && turn_number < maxTurnNumber) { + while(turn_number < maxTurnNumber && (std::count(result.begin(), result.end(), true) > 1 || (number_of_players == 1 && productive_squares_remaining > 0))) { //Increment turn number: turn_number++; if(!quiet_output) std::cout << "Turn " << turn_number << "\n"; @@ -342,6 +349,10 @@ GameStatistics Halite::runGame(std::vector * names_, unsigned int s return last_territory_count[u1] < last_territory_count[u2]; }); for(auto a = newRankings.begin(); a != newRankings.end(); a++) rankings.push_back(*a); + + //Count productive squares remaining for Halite single-player game + productive_squares_remaining = 0; + for(unsigned short b = 0; b < game_map.map_height; b++) for(unsigned short c = 0; c < game_map.map_width; c++) if(game_map.contents[b][c].owner == 0 && game_map.contents[b][c].production > 0) productive_squares_remaining++; result = newResult; } std::vector newRankings; @@ -359,6 +370,10 @@ GameStatistics Halite::runGame(std::vector * names_, unsigned int s PlayerStatistics p; p.tag = a + 1; p.rank = std::distance(rankings.begin(), std::find(rankings.begin(), rankings.end(), a)) + 1; + // alive_frame_count counts frames, but the frames are 0-base indexed (at least in the visualizer), so everyone needs -1 to find the frame # where last_alive + // however, the first place player and 2nd place player always have the same reported alive_frame_count (not sure why) + // it turns out to make "last_frame_alive" match what is seen in replayer, we have to -2 from all but finishers who are alive in last frame of game who only need -1 + p.last_frame_alive = alive_frame_count[a] - 2 + result[a]; p.average_territory_count = full_territory_count[a] / double(chunkSize * alive_frame_count[a]); p.average_strength_count = full_strength_count[a] / double(chunkSize * alive_frame_count[a]); p.average_production_count = alive_frame_count[a] > 1 ? full_production_count[a] / double(chunkSize * (alive_frame_count[a] - 1)) : 0; //For this, we want turns rather than frames. @@ -370,16 +385,18 @@ GameStatistics Halite::runGame(std::vector * names_, unsigned int s stats.timeout_tags = timeout_tags; stats.timeout_log_filenames = std::vector(timeout_tags.size()); //Output gamefile. First try the replays folder; if that fails, just use the straight filename. - stats.output_filename = "Replays/" + std::to_string(id) + '-' + std::to_string(seed) + ".hlt"; - try { - output(stats.output_filename); - } - catch(std::runtime_error & e) { - stats.output_filename = stats.output_filename.substr(8); - output(stats.output_filename); + if (enabledReplay) { + stats.output_filename = replayDirectory + "Replays/" + std::to_string(id) + '-' + std::to_string(seed) + ".hlt"; + try { + output(stats.output_filename); + } + catch(std::runtime_error & e) { + stats.output_filename = replayDirectory + std::to_string(id) + '-' + std::to_string(seed) + ".hlt"; + output(stats.output_filename); + } + if(!quiet_output) std::cout << "Map seed was " << seed << std::endl << "Opening a file at " << stats.output_filename << std::endl; + else std::cout << stats.output_filename << ' ' << seed << std::endl; } - if(!quiet_output) std::cout << "Map seed was " << seed << std::endl << "Opening a file at " << stats.output_filename << std::endl; - else std::cout << stats.output_filename << ' ' << seed << std::endl; //Output logs for players that timed out or errored. int timeoutIndex = 0; for(auto a = timeout_tags.begin(); a != timeout_tags.end(); a++) { diff --git a/environment/core/Halite.hpp b/environment/core/Halite.hpp index 5dddfe80..48b80bc2 100644 --- a/environment/core/Halite.hpp +++ b/environment/core/Halite.hpp @@ -19,6 +19,7 @@ extern bool quiet_output; struct PlayerStatistics { int tag; int rank; + int last_frame_alive; double average_territory_count; double average_strength_count; double average_production_count; @@ -27,7 +28,7 @@ struct PlayerStatistics { double average_frame_response_time; }; static std::ostream & operator<<(std::ostream & o, const PlayerStatistics & p) { - o << p.tag << ' ' << p.rank;// << ' ' << p.average_territory_count << ' ' << p.average_strength_count << ' ' << p.average_production_count << ' ' << p.still_percentage << ' ' << p.average_response_time; + o << p.tag << ' ' << p.rank << ' ' << p.last_frame_alive;// << ' ' << p.average_territory_count << ' ' << p.average_strength_count << ' ' << p.average_production_count << ' ' << p.still_percentage << ' ' << p.average_response_time; return o; } @@ -55,6 +56,8 @@ class Halite { //Game state unsigned short turn_number; unsigned short number_of_players; + unsigned short productive_squares_remaining; + unsigned short n_players_for_map_creation; bool ignore_timeout; hlt::Map game_map; std::vector player_names; @@ -80,9 +83,9 @@ class Halite { std::vector processNextFrame(std::vector alive); void output(std::string filename); public: - Halite(unsigned short width_, unsigned short height_, unsigned int seed_, Networking networking_, bool shouldIgnoreTimeout); + Halite(unsigned short width_, unsigned short height_, unsigned int seed_, unsigned short n_players_for_map_creation, Networking networking_, bool shouldIgnoreTimeout); - GameStatistics runGame(std::vector * names_, unsigned int seed, unsigned int id); + GameStatistics runGame(std::vector * names_, unsigned int seed, unsigned int id, bool enabledReplay, std::string replayDirectory); std::string getName(unsigned char playerTag); ~Halite(); diff --git a/environment/core/hlt.hpp b/environment/core/hlt.hpp index 120cd4ee..b07d1a51 100644 --- a/environment/core/hlt.hpp +++ b/environment/core/hlt.hpp @@ -203,9 +203,11 @@ namespace hlt{ } } + //Next, let's apply our shifts to create the shifts map. std::vector< std::vector > shifts = std::vector< std::vector >(map_height, std::vector(map_width, { 0, 0, 0 })); - if(preferHorizontal) { + if(numberOfPlayers == 6) shifts = reflections; + else if(preferHorizontal) { int shift = (prg() % dw) * (map_height / dw); //A vertical shift. for(int a = 0; a < dh; a++) { for(int b = 0; b < dw; b++) { @@ -279,6 +281,7 @@ namespace hlt{ contents[a][b].production = round(normalized[a][b].production * TOP_PROD); if(contents[a][b].owner != 0 && contents[a][b].production == 0) contents[a][b].production = 1; } + std::cout << map_width << " " << map_height << std::endl; } bool inBounds(Location l) const { diff --git a/environment/main.cpp b/environment/main.cpp index fc4e50a5..2505ca83 100644 --- a/environment/main.cpp +++ b/environment/main.cpp @@ -31,26 +31,23 @@ void promptDimensions(unsigned short & w, unsigned short & h); int main(int argc, char ** argv) { srand(time(NULL)); //For all non-seeded randomness. - if(argc == 1) { - std::cout << "You've provided the environment with no arguments.\n" - << "If this was intentional, please ignore this message.\n" - << "Else, please use the --help flag for usage details.\n"; - } - Networking networking; std::vector * names = NULL; unsigned int id = std::chrono::duration_cast(std::chrono::high_resolution_clock().now().time_since_epoch()).count(); - TCLAP::CmdLine cmd("Halite Game Environment", ' ', "1.0.1"); + TCLAP::CmdLine cmd("Halite Game Environment", ' ', "1.2"); //Switch Args. TCLAP::SwitchArg quietSwitch("q", "quiet", "Runs game in quiet mode, producing machine-parsable output.", cmd, false); TCLAP::SwitchArg overrideSwitch("o", "override", "Overrides player-sent names using cmd args [SERVER ONLY].", cmd, false); TCLAP::SwitchArg timeoutSwitch("t", "timeout", "Causes game environment to ignore timeouts (give all bots infinite time).", cmd, false); + TCLAP::SwitchArg noReplaySwitch("r", "noreplay", "Turns off the replay generation.", cmd, false); //Value Args + TCLAP::ValueArg nPlayersArg("n", "nplayers", "Create a map that will accommodate n players [SINGLE PLAYER MODE ONLY].", false, 1, "{1,2,3,4,5,6}", cmd); TCLAP::ValueArg< std::pair > dimensionArgs("d", "dimensions", "The dimensions of the map.", false, { 0, 0 }, "a string containing two space-seprated positive integers", cmd); TCLAP::ValueArg seedArg("s", "seed", "The seed for the map generator.", false, 0, "positive integer", cmd); + TCLAP::ValueArg replayDirectoryArg("i", "replaydirectory", "The path to directory for replay output.", false, ".", "path to directory", cmd); //Remaining Args, be they start commands and/or override names. Description only includes start commands since it will only be seen on local testing. TCLAP::UnlabeledMultiArg otherArgs("NonspecifiedArgs", "Start commands for bots.", false, "Array of strings", cmd); @@ -64,6 +61,8 @@ int main(int argc, char ** argv) { if(seedArg.getValue() != 0) seed = seedArg.getValue(); else seed = (std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count() % 4294967295); + unsigned short n_players_for_map_creation = nPlayersArg.getValue(); + quiet_output = quietSwitch.getValue(); bool override_names = overrideSwitch.getValue(); bool ignore_timeout = timeoutSwitch.getValue(); @@ -75,13 +74,15 @@ int main(int argc, char ** argv) { } if(mapWidth == 0 && mapHeight == 0) { - promptDimensions(mapWidth, mapHeight); + std::vector mapSizeChoices = {20, 25, 25, 30, 30, 30, 35, 35, 35, 35, 40, 40, 40, 45, 45, 50}; + mapWidth = mapSizeChoices[rand() % mapSizeChoices.size()]; + mapHeight = mapWidth; } if(override_names) { if(unlabeledArgs.size() < 4 || unlabeledArgs.size() % 2 != 0) { - std::cout << "Invalid player parameters from argv. Prompting instead (override disabled):" << std::endl; - networking = promptNetworking(); + std::cout << "Invalid number of player parameters with override switch enabled. Override intended for server use only." << std::endl; + exit(1); } else { try { @@ -94,17 +95,18 @@ int main(int argc, char ** argv) { } } catch(...) { - std::cout << "Invalid player parameters from argv. Prompting instead (override disabled):" << std::endl; - networking = promptNetworking(); + std::cout << "Invalid player parameters with override switch enabled. Override intended for server use only." << std::endl; delete names; names = NULL; + exit(1); } } } else { - if(unlabeledArgs.size() < 2) { - std::cout << "Invalid player parameters from argv. Prompting instead:" << std::endl; - networking = promptNetworking(); + if(unlabeledArgs.size() < 1) { + std::cout << "Please provide the launch command string for at least one bot." << std::endl + << "Use the --help flag for usage details.\n"; + exit(1); } try { while(!unlabeledArgs.empty()) { @@ -114,15 +116,35 @@ int main(int argc, char ** argv) { } } catch(...) { - std::cout << "Invalid player parameters from argv. Prompting instead:" << std::endl; - networking = promptNetworking(); + std::cout << "One or more of your bot launch command strings failed. Please check for correctness and try again." << std::endl; + exit(1); } } - //Create game. Null parameters will be ignored. - my_game = new Halite(mapWidth, mapHeight, seed, networking, ignore_timeout); + if(networking.numberOfPlayers() > 1 && n_players_for_map_creation != 1) { + std::cout << std::endl << "Only single-player mode enables specified n-player maps. When entering multiple bots, please do not try to specify n." << std::endl << std::endl; + exit(1); + } + + if(networking.numberOfPlayers() > 1) n_players_for_map_creation = networking.numberOfPlayers(); + + if(n_players_for_map_creation > 6 || n_players_for_map_creation < 1) { + std::cout << std::endl << "A map can only accommodate between 1 and 6 players." << std::endl << std::endl; + exit(1); + } + - GameStatistics stats = my_game->runGame(names, seed, id); + + //Create game. Null parameters will be ignored. + my_game = new Halite(mapWidth, mapHeight, seed, n_players_for_map_creation, networking, ignore_timeout); + + std::string outputFilename = replayDirectoryArg.getValue(); +#ifdef _WIN32 + if(outputFilename.back() != '\\') outputFilename.push_back('\\'); +#else + if(outputFilename.back() != '/') outputFilename.push_back('/'); +#endif + GameStatistics stats = my_game->runGame(names, seed, id, !noReplaySwitch.getValue(), outputFilename); if(names != NULL) delete names; std::string victoryOut; @@ -130,74 +152,10 @@ int main(int argc, char ** argv) { std::cout << stats; } else { - for(unsigned int a = 0; a < stats.player_statistics.size(); a++) std::cout << "Player #" << stats.player_statistics[a].tag << ", " << my_game->getName(stats.player_statistics[a].tag) << ", came in rank #" << stats.player_statistics[a].rank << "!\n"; + for(unsigned int a = 0; a < stats.player_statistics.size(); a++) std::cout << "Player #" << stats.player_statistics[a].tag << ", " << my_game->getName(stats.player_statistics[a].tag) << ", came in rank #" << stats.player_statistics[a].rank << " and was last alive on frame #" << stats.player_statistics[a].last_frame_alive << "!\n"; } delete my_game; return 0; } - -Networking promptNetworking() { - Networking n; - std::string in; - bool done = false; - for(int np = 0; !done; np++) { - //If less than 2, bypass this step: Ask if the user like to add another AI - if (np >= 2) { - std::cout << "Would you like to add another player? Please enter Yes or No: "; - while (true) { - std::getline(std::cin, in); - std::transform(in.begin(), in.end(), in.begin(), ::tolower); - if (in == "n" || in == "no" || in == "nope" || in == "y" || in == "yes" || in == "yep") break; - std::cout << "That isn't a valid input. Please enter Yes or No: "; - } - if (in == "n" || in == "no" || in == "nope") break; - } - - while (true) { - std::string startCommand; - std::cout << "What is the start command for this bot: "; - std::getline(std::cin, startCommand); - - try{ - n.startAndConnectBot(startCommand); - break; - } - catch (int e) { - std::cout << "There was a problem with that start command. Please enter another one.\n"; - } - } - - std::cout << "Connected to player #" << int(np + 1) << std::endl; - } - return n; -} - -void promptDimensions(unsigned short & w, unsigned short & h) { - std::string in; - std::cout << "Please enter the width of the map: "; - std::getline(std::cin, in); - while(true) { - try{ - w = std::stoi(in); - break; - } - catch(std::exception e) { - std::cout << "That isn't a valid input. Please enter a positive integer width of the map: "; - std::getline(std::cin, in); - } - } - std::cout << "Please enter the height of the map: "; - std::getline(std::cin, in); - while(true) { - try{ - h = std::stoi(in); - break; - } - catch(std::exception e) { - std::cout << "That isn't a valid input. Please enter a positive integer height of the map: "; - std::getline(std::cin, in); - } - } -} diff --git a/environment/make.bat b/environment/make.bat index 5cd60495..1e70e7f4 100644 --- a/environment/make.bat +++ b/environment/make.bat @@ -1 +1,4 @@ -g++ -o2 -std=c++11 main.cpp core/Halite.cpp -I . networking/Networking.cpp -o halite.exe -static +SET PATH=C:\Program Files (x86)\MSBuild\14.0\Bin;%PATH% +CALL "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64 +cd environment +cl.exe /O2 /MT /EHsc main.cpp core/Halite.cpp /I . networking/Networking.cpp /link /out:halite.exe diff --git a/environment/networking/Networking.cpp b/environment/networking/Networking.cpp index 0b88d7c6..1741c9c9 100644 --- a/environment/networking/Networking.cpp +++ b/environment/networking/Networking.cpp @@ -109,9 +109,12 @@ void Networking::sendString(unsigned char playerTag, std::string &sendString) { #endif } -std::string Networking::getString(unsigned char playerTag, unsigned int timeoutMillis) { +std::string Networking::getString(unsigned char playerTag, const unsigned int timeoutMillis) { std::string newString; + int timeoutMillisRemaining = timeoutMillis; + std::chrono::high_resolution_clock::time_point tp = std::chrono::high_resolution_clock::now(); + #ifdef _WIN32 WinConnection connection = connections[playerTag - 1]; @@ -120,7 +123,9 @@ std::string Networking::getString(unsigned char playerTag, unsigned int timeoutM char buffer; //Keep reading char by char until a newline - while (true) { + while(true) { + timeoutMillisRemaining = timeoutMillis - std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - tp).count(); + if(timeoutMillisRemaining < 0) throw newString; //Check to see that there are bytes in the pipe before reading //Throw error if no bytes in alloted time //Check for bytes before sampling clock, because reduces latency (vast majority the pipe is alread full) @@ -129,7 +134,7 @@ std::string Networking::getString(unsigned char playerTag, unsigned int timeoutM if(bytesAvailable < 1) { std::chrono::high_resolution_clock::time_point initialTime = std::chrono::high_resolution_clock::now(); while (bytesAvailable < 1) { - if(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - initialTime).count() > timeoutMillis) throw newString; + if(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - initialTime).count() > timeoutMillisRemaining) throw newString; PeekNamedPipe(connection.read, NULL, 0, NULL, &bytesAvailable, NULL); } } @@ -152,20 +157,17 @@ std::string Networking::getString(unsigned char playerTag, unsigned int timeoutM fd_set set; FD_ZERO(&set); /* clear the set */ FD_SET(connection.read, &set); /* add our file descriptor to the set */ - - - std::chrono::high_resolution_clock::time_point tp = std::chrono::high_resolution_clock::now(); char buffer; //Keep reading char by char until a newline while(true) { //Check if there are bytes in the pipe - timeoutMillis -= std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - tp).count(); - tp = std::chrono::high_resolution_clock::now(); + timeoutMillisRemaining = timeoutMillis - std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - tp).count(); + if(timeoutMillisRemaining < 0) throw newString; struct timeval timeout; - timeout.tv_sec = timeoutMillis / 1000.0; - timeout.tv_usec = (timeoutMillis % 1000)*1000; + timeout.tv_sec = timeoutMillisRemaining / 1000.0; + timeout.tv_usec = (timeoutMillisRemaining % 1000)*1000; int selectionResult = select(connection.read+1, &set, NULL, NULL, &timeout); if(selectionResult > 0) { @@ -258,7 +260,7 @@ void Networking::startAndConnectBot(std::string command) { if(!quiet_output) std::cout << command << "\n"; - pid_t pid = (pid_t)NULL; + pid_t pid; int writePipe[2]; int readPipe[2]; @@ -278,6 +280,7 @@ void Networking::startAndConnectBot(std::string command) { if(pid == 0) { //This is the child setpgid(getpid(), getpid()); +#ifdef __linux__ // install a parent death signal // http://stackoverflow.com/a/36945270 int r = prctl(PR_SET_PDEATHSIG, SIGTERM); @@ -288,6 +291,7 @@ void Networking::startAndConnectBot(std::string command) { } if (getppid() != ppid_before_fork) exit(1); +#endif dup2(writePipe[0], STDIN_FILENO); @@ -317,9 +321,9 @@ void Networking::startAndConnectBot(std::string command) { } int Networking::handleInitNetworking(unsigned char playerTag, const hlt::Map & m, bool ignoreTimeout, std::string * playerName) { - - const int ALLOTTED_MILLIS = ignoreTimeout ? 2147483647 : 15000; - + + const int ALLOTTED_MILLIS = ignoreTimeout ? 2147483647 : 30000; + std::string response; try { std::string playerTagString = std::to_string(playerTag), mapSizeString = serializeMapSize(m), mapString = serializeMap(m), prodString = serializeProductions(m); @@ -362,7 +366,7 @@ int Networking::handleInitNetworking(unsigned char playerTag, const hlt::Map & m int Networking::handleFrameNetworking(unsigned char playerTag, const unsigned short & turnNumber, const hlt::Map & m, bool ignoreTimeout, std::map * moves) { const int ALLOTTED_MILLIS = ignoreTimeout ? 2147483647 : 1500; - + std::string response; try { if(isProcessDead(playerTag)) return -1; diff --git a/environment/networking/Networking.hpp b/environment/networking/Networking.hpp index 252a3a0c..ccca8c51 100644 --- a/environment/networking/Networking.hpp +++ b/environment/networking/Networking.hpp @@ -16,7 +16,9 @@ #include #include #include +#ifdef __linux__ #include +#endif #include #endif diff --git a/tests/environment/testenv.py b/tests/environment/testenv.py index 1d6e842d..da70b5ee 100644 --- a/tests/environment/testenv.py +++ b/tests/environment/testenv.py @@ -39,7 +39,7 @@ # Ensures that the environment can run a basic game where a bot wins. Confirm that the bot expected to win does indeed win. genlines = subprocess.Popen('../../environment/halite -d "10 10" -q "python3 ModBot.py" "python3 ModBot.py" -s 1001', stdout=subprocess.PIPE, shell = True).stdout.read().decode('utf-8').split('\n') -if genlines[3].split()[1] != "1" or genlines[4].split()[1] != "2" or genlines[5] != " " or genlines[6] != " ": +if genlines[-4].split()[1] != "1" or genlines[-3].split()[1] != "2" or genlines[-2] != " " or genlines[-1] != " ": print('General environment test failed. Environment output:\n#######################################################') print('\n'.join(genlines) + '\n#######################################################') isGood = False @@ -47,17 +47,20 @@ print('General environment test succeeded.') # Ensures that the environment can run a basic game where a bot wins. Confirm that the bot expected to win does indeed win. -splines = subprocess.Popen('../../environment/halite -d "10 10" -q "../../airesources/C++/MyBot" "cd ../../airesources/Java; java MyBot" "python3 ../../airesources/Python/MyBot.py" "../../airesources/Rust/target/release/MyBot" -s 1000', stdout=subprocess.PIPE, shell = True).stdout.read().decode('utf-8').split('\n') -if splines[9] != " " or splines[10] != " ": +splines = subprocess.Popen('../../environment/halite -d "10 10" -q "../../airesources/C++/MyBot" "java -cp ../../airesources/Java MyBot" "python3 ../../airesources/Python/MyBot.py" "../../airesources/Rust/target/release/MyBot" -s 1000', stdout=subprocess.PIPE, shell = True).stdout.read().decode('utf-8').split('\n') +if splines[-2] != " " or splines[-1] != " ": print('Starter package test failed. Environment output:\n#######################################################') print('\n'.join(splines) + '\n#######################################################') isGood = False else: print('Starter package test succeeded.') +splines = subprocess.Popen('cat *.log', stdout=subprocess.PIPE, shell = True).stdout.read().decode('utf-8').split('\n') +print('\n'.join(splines)) + # Ensures that tie evaluation is correct. Confirm that the bot expected to win does indeed win. tielines = subprocess.Popen('../../environment/halite -d "10 10" -q "python3 ModBot.py" "python3 ModBot.py" -s 998', stdout=subprocess.PIPE, shell = True).stdout.read().decode('utf-8').split('\n') -if tielines[3].split()[1] != "2" or tielines[4].split()[1] != "1" or tielines[5] != " " or tielines[6] != " ": +if tielines[-4].split()[1] != "2" or tielines[-3].split()[1] != "1" or tielines[-2] != " " or tielines[-1] != " ": print('Tie evaluation test failed. Environment output:\n#######################################################') print('\n'.join(tielines) + '\n#######################################################') isGood = False @@ -66,7 +69,7 @@ # Ensures that all timeouts work well. timelines = subprocess.Popen('../../environment/halite -d "20 20" -q "python3 FailInitBot.py" "python3 TimeoutInitBot.py" "python3 Fail10Bot.py" "python3 Timeout10Bot.py" "python3 ModBot.py" -s 998', stdout=subprocess.PIPE, shell = True).stdout.read().decode('utf-8').split('\n') -if timelines[6].split()[1] != "5" or timelines[7].split()[1] != "4" or timelines[8].split()[1] != "3" or timelines[9].split()[1] != "2" or timelines[10].split()[1] != "1" or timelines[11] != "1 2 3 4 ": +if timelines[-7].split()[1] != "5" or timelines[-6].split()[1] != "4" or timelines[-5].split()[1] != "3" or timelines[-4].split()[1] != "2" or timelines[-3].split()[1] != "1" or timelines[-2] != "1 2 3 4 ": print('Timeout evaluation test failed. Environment output:\n#######################################################') print('\n'.join(timelines) + '\n#######################################################') isGood = False diff --git a/tests/install.sh b/tests/install.sh index 54aeca6d..fb9b09e8 100755 --- a/tests/install.sh +++ b/tests/install.sh @@ -2,13 +2,13 @@ add-apt-repository -y ppa:ubuntu-toolchain-r/test apt-get update # Python -apt-get install -y python3 +apt-get install -y python3 # Java -apt-get install -y openjdk-7-jdk libjansi-java +apt-get install -y openjdk-8-jdk libjansi-java # Rust -curl -sSf https://static.rust-lang.org/rustup.sh | sh +curl -sSf https://static.rust-lang.org/rustup.sh | sh # C++ apt-get install -y g++-4.9 @@ -24,11 +24,18 @@ apt-get update -y apt-get install -y sbt # Php unit -wget https://phar.phpunit.de/phpunit.phar -chmod +x phpunit.phar -mv phpunit.phar /usr/local/bin/phpunit +wget https://phar.phpunit.de/phpunit-5.7.phar +chmod +x phpunit-5.7.phar +mv phpunit-5.7.phar /usr/local/bin/phpunit php -v mysql -V phpunit --version + +update-alternatives --set java $(update-alternatives --list java | grep java-8-openjdk) +update-alternatives --set javac $(update-alternatives --list javac | grep java-8-openjdk) +update-alternatives --display java +update-alternatives --display javac +java -version +javac -version diff --git a/tests/worker/languageBot/LANGUAGE b/tests/worker/languageBot/LANGUAGE new file mode 100644 index 00000000..b257c0e7 --- /dev/null +++ b/tests/worker/languageBot/LANGUAGE @@ -0,0 +1 @@ +TestLanguage diff --git a/tests/worker/languageBot/MyBot.py b/tests/worker/languageBot/MyBot.py new file mode 120000 index 00000000..890e12ef --- /dev/null +++ b/tests/worker/languageBot/MyBot.py @@ -0,0 +1 @@ +../../../airesources/Python/MyBot.py \ No newline at end of file diff --git a/tests/worker/languageBot/hlt.py b/tests/worker/languageBot/hlt.py new file mode 120000 index 00000000..28bd5d96 --- /dev/null +++ b/tests/worker/languageBot/hlt.py @@ -0,0 +1 @@ +../../../airesources/Python/hlt.py \ No newline at end of file diff --git a/tests/worker/languageBot/networking.py b/tests/worker/languageBot/networking.py new file mode 120000 index 00000000..926f2430 --- /dev/null +++ b/tests/worker/languageBot/networking.py @@ -0,0 +1 @@ +../../../airesources/Python/networking.py \ No newline at end of file diff --git a/tests/worker/languageBot/run.sh b/tests/worker/languageBot/run.sh new file mode 100755 index 00000000..896ae519 --- /dev/null +++ b/tests/worker/languageBot/run.sh @@ -0,0 +1,2 @@ +#Python +python3 MyBot.py diff --git a/tests/worker/loseBot/MyBot.py b/tests/worker/loseBot/MyBot.py index 6cf338e3..83501506 100644 --- a/tests/worker/loseBot/MyBot.py +++ b/tests/worker/loseBot/MyBot.py @@ -1,25 +1,12 @@ -from hlt import * -from networking import * +import hlt +from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square +import random -myID, gameMap = getInit() -sendInit("BasicPythonBot") -while True: - moves = [] - gameMap = getFrame() - - for y in range(gameMap.height): - for x in range(gameMap.width): - site = gameMap.getSite(Location(x, y)) - if site.owner == myID: - direction = random.randint(0, 5) - if site.strength < 5*site.production: - direction = STILL - else: - for d in CARDINALS: - if gameMap.getSite(Location(x, y), d).owner != myID: - direction = d - break - moves.append(Move(Location(x, y), direction)) +myID, game_map = hlt.get_init() +hlt.send_init("MyPythonBot") - sendFrame(moves) +while True: + game_map.get_frame() + moves = [Move(square, random.choice((NORTH, EAST, SOUTH, WEST, STILL))) for square in game_map if square.owner == myID] + hlt.send_frame(moves) diff --git a/tests/worker/loseBot/hlt.py b/tests/worker/loseBot/hlt.py deleted file mode 100644 index 254f8745..00000000 --- a/tests/worker/loseBot/hlt.py +++ /dev/null @@ -1,96 +0,0 @@ -import random -import math -import copy - -STILL = 0 -NORTH = 1 -EAST = 2 -SOUTH = 3 -WEST = 4 - -DIRECTIONS = [a for a in range(0, 5)] -CARDINALS = [a for a in range(1, 5)] - -ATTACK = 0 -STOP_ATTACK = 1 - -class Location: - def __init__(self, x=0, y=0): - self.x = x - self.y = y -class Site: - def __init__(self, owner=0, strength=0, production=0): - self.owner = owner - self.strength = strength - self.production = production -class Move: - def __init__(self, loc=0, direction=0): - self.loc = loc - self.direction = direction - -class GameMap: - def __init__(self, width = 0, height = 0, numberOfPlayers = 0): - self.width = width - self.height = height - self.contents = [] - - for y in range(0, self.height): - row = [] - for x in range(0, self.width): - row.append(Site(0, 0, 0)) - self.contents.append(row) - - def inBounds(self, l): - return l.x >= 0 and l.x < self.width and l.y >= 0 and l.y < self.height - - def getDistance(self, l1, l2): - dx = math.abs(l1.x - l2.x) - dy = math.abs(l1.y - l2.y) - if dx > self.width / 2: - dx = self.width - dx - if dy > self.height / 2: - dy = self.height - dy - return dx + dy - - def getAngle(self, l1, l2): - dx = l2.x - l1.x - dy = l2.y - l1.y - - if dx > self.width - dx: - dx -= self.width - elif -dx > self.width + dx: - dx += self.width - - if dy > self.height - dy: - dy -= self.height - elif -dy > self.height + dy: - dy += self.height - return math.atan2(dy, dx) - - def getLocation(self, loc, direction): - l = copy.deepcopy(loc) - if direction != STILL: - if direction == NORTH: - if l.y == 0: - l.y = self.height - 1 - else: - l.y -= 1 - elif direction == EAST: - if l.x == self.width - 1: - l.x = 0 - else: - l.x += 1 - elif direction == SOUTH: - if l.y == self.height - 1: - l.y = 0 - else: - l.y += 1 - elif direction == WEST: - if l.x == 0: - l.x = self.width - 1 - else: - l.x -= 1 - return l - def getSite(self, l, direction = STILL): - l = self.getLocation(l, direction) - return self.contents[l.y][l.x] diff --git a/tests/worker/loseBot/hlt.py b/tests/worker/loseBot/hlt.py new file mode 120000 index 00000000..28bd5d96 --- /dev/null +++ b/tests/worker/loseBot/hlt.py @@ -0,0 +1 @@ +../../../airesources/Python/hlt.py \ No newline at end of file diff --git a/tests/worker/loseBot/networking.py b/tests/worker/loseBot/networking.py deleted file mode 100644 index df189b40..00000000 --- a/tests/worker/loseBot/networking.py +++ /dev/null @@ -1,84 +0,0 @@ -from hlt import * -import socket -import traceback -import struct -from ctypes import * -import sys - -_productions = [] -_width = -1 -_height = -1 - -def serializeMoveSet(moves): - returnString = "" - for move in moves: - returnString += str(move.loc.x) + " " + str(move.loc.y) + " " + str(move.direction) + " " - return returnString - -def deserializeMapSize(inputString): - splitString = inputString.split(" ") - - global _width, _height - _width = int(splitString.pop(0)) - _height = int(splitString.pop(0)) - -def deserializeProductions(inputString): - splitString = inputString.split(" ") - - for a in range(0, _height): - row = [] - for b in range(0, _width): - row.append(int(splitString.pop(0))) - _productions.append(row) - -def deserializeMap(inputString): - splitString = inputString.split(" ") - - m = GameMap(_width, _height) - - y = 0 - x = 0 - counter = 0 - owner = 0 - while y != m.height: - counter = int(splitString.pop(0)) - owner = int(splitString.pop(0)) - for a in range(0, counter): - m.contents[y][x].owner = owner - x += 1 - if x == m.width: - x = 0 - y += 1 - - for a in range(0, _height): - for b in range(0, _width): - m.contents[a][b].strength = int(splitString.pop(0)) - m.contents[a][b].production = _productions[a][b] - - return m - -def sendString(toBeSent): - toBeSent += '\n' - - sys.stdout.write(toBeSent) - sys.stdout.flush() - -def getString(): - return sys.stdin.readline().rstrip('\n') - -def getInit(): - playerTag = int(getString()) - deserializeMapSize(getString()) - deserializeProductions(getString()) - m = deserializeMap(getString()) - - return (playerTag, m) - -def sendInit(name): - sendString(name) - -def getFrame(): - return deserializeMap(getString()) - -def sendFrame(moves): - sendString(serializeMoveSet(moves)) diff --git a/tests/worker/testWorker.py b/tests/worker/testWorker.py index ebc53f08..4bf12226 100644 --- a/tests/worker/testWorker.py +++ b/tests/worker/testWorker.py @@ -43,6 +43,20 @@ def testStarterPackages(self): assert language == expectedLanguage assert errors == None + def testLanguageOverride(self): + '''Use a LANGUAGE file to override the detected language''' + LANGUAGE_BOT_PATH = "languageBot" + + bot_dir = os.path.join(OUR_PATH, LANGUAGE_BOT_PATH) + expectedLanguage = "TestLanguage" + + language, errors = compiler.compile_anything(bot_dir) + if errors is not None: print("Errors: " + "\n".join(errors)) + print("Language: " + language) + + assert language == expectedLanguage + assert errors == None + class GameTests(unittest.TestCase): def testNormalGame(self): '''Test the parsing of the output of runGame.sh''' @@ -60,23 +74,27 @@ def testNormalGame(self): output = worker.runGame(20, 20, [{"userID": WIN_BOT_PATH, "username": WIN_BOT_PATH, "numSubmissions": "1"}, {"userID": LOSE_BOT_PATH, "username": LOSE_BOT_PATH, "numSubmissions": "1"}]) os.chdir(OUR_PATH) - assert int(output[len(output)-4].split(" ")[0]) == 1 - assert int(output[len(output)-3].split(" ")[0]) == 2 + assert int(output[len(output)-4].split(" ")[1]) == 1 + assert int(output[len(output)-3].split(" ")[1]) == 2 def testParsing(self): '''Test the parsing of the output of runGame.sh''' + MAP_SIZE = (35, 35) REPLAY_FILE = "123456.hlt" SEED = 123 USERS = [{"playerTag": 1, "rank": 2, "territoryAverage": 0.5, "strengthAverage": 0.6, "productionAverage": 0.7, "stillPercentage": 0.8, "turnTimeAverage": 0.9, "didTimeout": True, "errorLogName": "errorLog.log"}, {"playerTag": 2, "rank": 1, "territoryAverage": 1.5, "strengthAverage": 1.6, "productionAverage": 1.7, "stillPercentage": 1.8, "turnTimeAverage": 1.9, "didTimeout": False, "errorLogName": None}] ERROR_LOGS = [str(user['errorLogName']) for user in USERS if user["didTimeout"] == True] - lines = ["%s %d" % (REPLAY_FILE, SEED)] + lines = ["%d %d" % MAP_SIZE] + lines += ["./%s %d" % (REPLAY_FILE, SEED)] for user in USERS: lines += ["%d %d %f %f %f %f %f" % (user['playerTag'], user['rank'], user['territoryAverage'], user['strengthAverage'], user['productionAverage'], user['stillPercentage'], user['turnTimeAverage'])] lines += [str(user['playerTag']) for user in USERS if user["didTimeout"] == True] lines += ERROR_LOGS - outputUsers, outputReplay, outputErrorLogs = worker.parseGameOutput(lines, USERS) + outputWidth, outputHeight, outputUsers, outputReplay, outputErrorLogs = worker.parseGameOutput(lines, USERS) + assert outputWidth == MAP_SIZE[0] + assert outputHeight == MAP_SIZE[1] assert outputUsers == USERS assert outputReplay == REPLAY_FILE assert outputErrorLogs == ERROR_LOGS diff --git a/tests/worker/winBot/MyBot.py b/tests/worker/winBot/MyBot.py index b531fefd..0555d451 100644 --- a/tests/worker/winBot/MyBot.py +++ b/tests/worker/winBot/MyBot.py @@ -1,14 +1,23 @@ -from hlt import * -from networking import * +import hlt +from hlt import NORTH, EAST, SOUTH, WEST, STILL, Move, Square +import random + + +myID, game_map = hlt.get_init() +hlt.send_init("PythonBot") + +def assign_move(square): + for direction, neighbor in enumerate(game_map.neighbors(square)): + if neighbor.owner != myID and neighbor.strength < square.strength: + return Move(square, direction) + + if square.strength < 5 * square.production: + return Move(square, STILL) + else: + return Move(square, random.choice((WEST, NORTH))) -myID, gameMap = getInit() -sendInit("PythonBot") while True: - moves = [] - gameMap = getFrame() - for y in range(gameMap.height): - for x in range(gameMap.width): - if gameMap.getSite(Location(x, y)).owner == myID: - moves.append(Move(Location(x, y), int(random.random() * 5))) - sendFrame(moves) + game_map.get_frame() + moves = [assign_move(square) for square in game_map if square.owner == myID] + hlt.send_frame(moves) diff --git a/tests/worker/winBot/hlt.py b/tests/worker/winBot/hlt.py deleted file mode 100644 index 254f8745..00000000 --- a/tests/worker/winBot/hlt.py +++ /dev/null @@ -1,96 +0,0 @@ -import random -import math -import copy - -STILL = 0 -NORTH = 1 -EAST = 2 -SOUTH = 3 -WEST = 4 - -DIRECTIONS = [a for a in range(0, 5)] -CARDINALS = [a for a in range(1, 5)] - -ATTACK = 0 -STOP_ATTACK = 1 - -class Location: - def __init__(self, x=0, y=0): - self.x = x - self.y = y -class Site: - def __init__(self, owner=0, strength=0, production=0): - self.owner = owner - self.strength = strength - self.production = production -class Move: - def __init__(self, loc=0, direction=0): - self.loc = loc - self.direction = direction - -class GameMap: - def __init__(self, width = 0, height = 0, numberOfPlayers = 0): - self.width = width - self.height = height - self.contents = [] - - for y in range(0, self.height): - row = [] - for x in range(0, self.width): - row.append(Site(0, 0, 0)) - self.contents.append(row) - - def inBounds(self, l): - return l.x >= 0 and l.x < self.width and l.y >= 0 and l.y < self.height - - def getDistance(self, l1, l2): - dx = math.abs(l1.x - l2.x) - dy = math.abs(l1.y - l2.y) - if dx > self.width / 2: - dx = self.width - dx - if dy > self.height / 2: - dy = self.height - dy - return dx + dy - - def getAngle(self, l1, l2): - dx = l2.x - l1.x - dy = l2.y - l1.y - - if dx > self.width - dx: - dx -= self.width - elif -dx > self.width + dx: - dx += self.width - - if dy > self.height - dy: - dy -= self.height - elif -dy > self.height + dy: - dy += self.height - return math.atan2(dy, dx) - - def getLocation(self, loc, direction): - l = copy.deepcopy(loc) - if direction != STILL: - if direction == NORTH: - if l.y == 0: - l.y = self.height - 1 - else: - l.y -= 1 - elif direction == EAST: - if l.x == self.width - 1: - l.x = 0 - else: - l.x += 1 - elif direction == SOUTH: - if l.y == self.height - 1: - l.y = 0 - else: - l.y += 1 - elif direction == WEST: - if l.x == 0: - l.x = self.width - 1 - else: - l.x -= 1 - return l - def getSite(self, l, direction = STILL): - l = self.getLocation(l, direction) - return self.contents[l.y][l.x] diff --git a/tests/worker/winBot/hlt.py b/tests/worker/winBot/hlt.py new file mode 120000 index 00000000..28bd5d96 --- /dev/null +++ b/tests/worker/winBot/hlt.py @@ -0,0 +1 @@ +../../../airesources/Python/hlt.py \ No newline at end of file diff --git a/tests/worker/winBot/networking.py b/tests/worker/winBot/networking.py deleted file mode 100644 index df189b40..00000000 --- a/tests/worker/winBot/networking.py +++ /dev/null @@ -1,84 +0,0 @@ -from hlt import * -import socket -import traceback -import struct -from ctypes import * -import sys - -_productions = [] -_width = -1 -_height = -1 - -def serializeMoveSet(moves): - returnString = "" - for move in moves: - returnString += str(move.loc.x) + " " + str(move.loc.y) + " " + str(move.direction) + " " - return returnString - -def deserializeMapSize(inputString): - splitString = inputString.split(" ") - - global _width, _height - _width = int(splitString.pop(0)) - _height = int(splitString.pop(0)) - -def deserializeProductions(inputString): - splitString = inputString.split(" ") - - for a in range(0, _height): - row = [] - for b in range(0, _width): - row.append(int(splitString.pop(0))) - _productions.append(row) - -def deserializeMap(inputString): - splitString = inputString.split(" ") - - m = GameMap(_width, _height) - - y = 0 - x = 0 - counter = 0 - owner = 0 - while y != m.height: - counter = int(splitString.pop(0)) - owner = int(splitString.pop(0)) - for a in range(0, counter): - m.contents[y][x].owner = owner - x += 1 - if x == m.width: - x = 0 - y += 1 - - for a in range(0, _height): - for b in range(0, _width): - m.contents[a][b].strength = int(splitString.pop(0)) - m.contents[a][b].production = _productions[a][b] - - return m - -def sendString(toBeSent): - toBeSent += '\n' - - sys.stdout.write(toBeSent) - sys.stdout.flush() - -def getString(): - return sys.stdin.readline().rstrip('\n') - -def getInit(): - playerTag = int(getString()) - deserializeMapSize(getString()) - deserializeProductions(getString()) - m = deserializeMap(getString()) - - return (playerTag, m) - -def sendInit(name): - sendString(name) - -def getFrame(): - return deserializeMap(getString()) - -def sendFrame(moves): - sendString(serializeMoveSet(moves)) diff --git a/visualizer/index.html b/visualizer/index.html index b9135da5..64959842 100644 --- a/visualizer/index.html +++ b/visualizer/index.html @@ -9,19 +9,23 @@
-
+

Drop or upload a replay file here

+
+
- +
+ + diff --git a/visualizer/index.js b/visualizer/index.js index e127efe1..7f906427 100644 --- a/visualizer/index.js +++ b/visualizer/index.js @@ -7,28 +7,31 @@ $(function () { alert("An error ocurred reading the file :" + err.message); return; } - console.log(data) - showGame(textToGame(data, args[2]), $("#pageContent"), null, null, true, false, true); + console.log(data); + $("label[for=filePicker]").text("Select another file"); + var fsHeight = $("#fileSelect").outerHeight(); + showGame(textToGame(data, args[2]), $("#displayArea"), null, -fsHeight, true, false, true); }); } - var $dropZone = $("#dropZone"); + var $dropZone = $("html"); var $filePicker = $("#filePicker"); function handleFiles(files) { - for(var i=0, file; file=files[i]; i++) { - console.log(file) - var reader = new FileReader(); + // only use the first file. + file = files[0]; + console.log(file) + var reader = new FileReader(); - reader.onload = (function(filename) { // finished reading file data. - return function(e2) { - $dropZone.remove(); - showGame(textToGame(e2.target.result, filename), $("#pageContent"), null, null, true, false, true); - }; - })(file.name); - reader.readAsText(file); // start reading the file data. - } + reader.onload = (function(filename) { // finished reading file data. + return function(e2) { + $("#displayArea").empty(); + $("label[for=filePicker]").text("Select another file"); + var fsHeight = $("#fileSelect").outerHeight(); + showGame(textToGame(e2.target.result, filename), $("#displayArea"), null, -fsHeight, true, false, true); + }; + })(file.name); + reader.readAsText(file); // start reading the file data. } - $("#pageContent").append($dropZone); $dropZone.on('dragover', function(e) { e.stopPropagation(); diff --git a/visualizer/lib/lodash.min.js b/visualizer/lib/lodash.min.js new file mode 120000 index 00000000..d0e73a8e --- /dev/null +++ b/visualizer/lib/lodash.min.js @@ -0,0 +1 @@ +../../website/lib/lodash.min.js \ No newline at end of file diff --git a/visualizer/lib/xss.js b/visualizer/lib/xss.js new file mode 120000 index 00000000..d9d5112a --- /dev/null +++ b/visualizer/lib/xss.js @@ -0,0 +1 @@ +../../website/lib/xss.js \ No newline at end of file diff --git a/website/.htaccess b/website/.htaccess index 680e63a2..37ba7a99 100644 --- a/website/.htaccess +++ b/website/.htaccess @@ -11,6 +11,8 @@ AddOutputFilterByType DEFLATE application/x-javascript ErrorDocument 404 /404.php -ExpiresActive on -ExpiresDefault "access plus 2 days" + + ExpiresActive on + ExpiresDefault "access plus 2 days" + diff --git a/website/about.php b/website/about.php index 9ab499a0..6f3bf3a0 100644 --- a/website/about.php +++ b/website/about.php @@ -20,7 +20,7 @@

Halite is designed for all levels of coding enthusiasts—from high school students to senior engineers. The simplest Halite bot requires only 10 lines of code, but bots can get ever more advanced. Players may find themselves surprised: we’ve learned from thousands of hours of gameplay that it’s not always the most complicated algorithm that wins. Creativity and insight are highly rewarded! So code well — a bot’s fate depends on it.

-

Halite was conceived of by Benjamin Spector, who partnered with his high school class-mate Michael Truell to develop the game. After receiving positive feedback on the Halite, the pair continually revised the game, extended it into the cloud and developed additional execution, leaderboard, and web interface capabilities.

+

Halite was conceived of and developed by Benjamin Spector and Michael Truell. Ben largely wrote the game implementation and starter packages, while Michael was primarily responsible for the website and the competition backend infrastructure. After receiving positive feedback on the Halite, the pair continually revised the game, extended it into the cloud and developed additional execution, leaderboard, and web interface capabilities.

Two Sigma, having had a history of playful programming challenges for its mathematical and software-oriented teams (e.g., see the Robotic Air Hockey Competition) retained Ben and Michael as 2016 summer interns to further develop Halite and then run an internal Halite Challenge. The team incorporated feedback from many, and about thirty-five players competed in a successful 3-week tournament in August.

@@ -28,7 +28,7 @@

To help productize and orchestrate the public launch, Two Sigma reached out to Cornell Tech, a leader in graduate tech education. With a shared interest in encouraging programming, entrepreneurship, and growth of the thriving New York tech scene, the two partners initiated the public Halite competition in November 2016. Cornell Tech is providing ongoing game support and community management, and by working closely with the online gaming community, it will empower players to collaborate, learn and have fun.

-

Many have contributed to Halite beyond Ben’s and Michael’s invention and engineering; they include Eric Abrego, Matt Adereth, Trammel Hudson, Emily Malloy, Arnaud Sahuguet, and Scott Spinner. And, no doubt, much credit should also be provided to users who are sending questions and comments to the forums.

+

Many have contributed to Halite beyond Ben’s and Michael’s invention and engineering; they include Jaques Clapauch, Eric Abrego, Matt Adereth, Trammel Hudson, Emily Malloy, Arnaud Sahuguet, and Scott Spinner. And, no doubt, much credit should also be provided to users who are sending questions and comments to the forums.

May the best bot win!

diff --git a/website/advanced_command_line.php b/website/advanced_command_line.php index 80de8452..10a7231a 100644 --- a/website/advanced_command_line.php +++ b/website/advanced_command_line.php @@ -2,7 +2,7 @@ - Environment Command Reference + Halite Environment CLI @@ -14,7 +14,7 @@
-

Environment Command Reference

+

Halite Environment CLI

The Halite environment is responsible for running games between bots and outputting appropriate data and files upon the end of a game. The downloadable version is the same version used on the servers.

@@ -25,9 +25,12 @@
  • -t: disables timeouts for the duration of the game.
  • -q: turns on quiet output. Output will take the form of:
      +
    • Unless the -o option is also specified, lines with the command used to run each bot (one bot per line).
    • +
    • A line showing the actual map size used.
    • A line containing the replay file name, a space, and the map seed.
    • -
    • For n players in the game, n lines like so: playerID rank
    • +
    • For n players in the game, n lines like so: playerID rank lastFrameAlive
    • A line of space separated playerIDs of the players that timed out.
    • +
    • A line of space separated timeout log filenames.
  • -s: provides the seed to the map generator. If this is not provided, it will use a time-based seed.
  • diff --git a/website/guides_development.php b/website/advanced_development.php similarity index 66% rename from website/guides_development.php rename to website/advanced_development.php index d8568195..f2aa874e 100644 --- a/website/guides_development.php +++ b/website/advanced_development.php @@ -2,7 +2,7 @@ - Bot Development Guide + Best Practices @@ -14,17 +14,14 @@
    -

    Bot Development Guide

    -

    In this guide, we will detail a couple of useful practices to follow when building your Halite bot.

    +

    Best Practices

    +

    In this guide, we will list a couple of simple, useful practices to follow when building your Halite bot.

    Using a Log File

    -

    Stdout and stdin in are used to communicate with the game environment. As such, you cannot use functions like System.out.println, print(), or std::cout. Instead, print debugging information to a log file.

    +

    Stdout and stdin in are used to communicate with the game environment. As such, you cannot use functions like System.out.println, print(), or std::cout. Instead, print debugging information to a log file.

    Local Bot Evaluation

    Before submitting a new bot to the online leaderboard, we recommend running some games against the version of your bot that is currently on the leaderboard. If your new bot consistently wins, then put it up!

    Disabling the Timeout Flag

    -

    When debugging latency issues with your bot, it can be helpful to disable game environment timeouts. To do so, append the -t flag to your environment command (e.g. ./environment -d "30 30" "python3 MyBot.py" "python3 RandomBot.py" -t).

    -

    Understanding Game Logs

    -

    When your bot times out or errors on our game servers, we save and display a log file with debugging information including the time your bot took each turn, its output each turn, and its final output from stdout and stderr.

    -

    To find these log files, visit your homepage: https://halite.io/user.php. Just click the download log button to grab your error log for a game:

    +

    When debugging latency issues with your bot, it can be helpful to disable game environment timeouts. To do so, append the -t flag to your environment command (e.g. ./environment -d "30 30" "python3 MyBot.py" "python3 RandomBot.py" -t).

    Debugging with an IDE

    There is a community contributed method for running a Halite bot from a custom debugger locally. More on this can be found here on the forums.

    diff --git a/website/advanced_game_server.php b/website/advanced_game_server.php index aca1b53a..c39a1614 100644 --- a/website/advanced_game_server.php +++ b/website/advanced_game_server.php @@ -2,7 +2,7 @@ - Game Server Reference + Game Servers @@ -15,7 +15,7 @@
    -

    Game Server Reference

    +

    Game Servers

    Hardware

    All compilation and game execution is done on AWS EC2 m3.medium servers running Ubuntu 16.04. They have the following specs: @@ -31,7 +31,7 @@

    Compilation

    Bot compilation is done using this autocompile script.

    -

    To facilitate the installation of custom software, we allow users to include an install script. If a file named install.sh exists in your submission, it is run as a bash script under the root user in a sandbox with internet access and 10 minutes of runtime. Bots may only read and write to their current directory, so all files that you want to be available at runtime must be installed locally. For more on using 3rd party libraries, click here.

    +

    To facilitate the installation of custom software, we allow users to include an install script. If a file named install.sh exists in your submission, it is run as a bash script under the root user in a sandbox with internet access and 10 minutes of runtime. Bots may only read and write to their current directory, so all files that you want to be available at runtime must be installed locally. For more on using 3rd party libraries, click here.

    Your main file must be called MyBot. Your language is recognized using the file extension of your MyBot file. The appropriate file extensions for each language are:

      @@ -47,6 +47,8 @@
    • JavaScript - .js
    • OCaml - .ml
    • Clojure - .clj
    • +
    • C - .c
    • +
    • Julia - .jl

    @@ -54,10 +56,11 @@ The following compilers are used:
    • Java - javac 1.8.0_111
    • -
    • C++ - g++ 4.84
    • +
    • C++ - g++ 4.8
    • C# - mcs 4.6.1.0
    • Rust - rustc 1.10.0
    • Scala - scalac 2.10.4
    • +
    • C - gcc 4.8

    @@ -84,6 +87,7 @@
  • Node.js (JavaScript) 7.1.0
  • OCaml 4.01.0
  • Clojure 1.8.0
  • +
  • Julia 0.5.0
  • diff --git a/website/advanced_libraries.php b/website/advanced_libraries.php new file mode 100644 index 00000000..0ceb22e8 --- /dev/null +++ b/website/advanced_libraries.php @@ -0,0 +1,103 @@ + + + + + Submitting A Bot + + + + + + +
    + +
    + +
    +

    Submitting A Bot

    + +

    You submit a zip file containing your MyBot file and any other files needed to compile and run your MyBot file.

    + +

    Installing Dependencies

    + +

    Instead of packaging up your dependencies inside your zip file, you may include a bash file named install.sh that will be run before your bot is compiled.

    + +

    It will be run with internet access and write access to only its current directory. It may run for a maximum of 10 minutes and will not be able to make global installation changes (i.e. apt-get will not work).

    + +

    Package Managers

    + +

    The following package managers are already installed on the server and can be used to install dependencies locally:

    + +
      +
    • pip3
    • +
    • bundler
    • +
    • npm
    • +
    + +

    curl is also available and can be used to download additional runtimes, tools, and environments.

    + +

    If your library isn't on a package manager that supports local installation and you can’t download it with curl, you are going to have to compile it on our game servers. Include the source of you library in your bot's zip file and put compilation instructions in the install.sh file.

    + +

    Preinstalled Libraries

    +

    For convenience's sake, we include tensorflow, keras (using a tensorflow backend), numpy, scipy, scikit-learn, pillow, and h5py on our game servers. Just import these libraries from your python files like normal!

    + +

    Compilation

    + +

    Bot compilation is done using this autocompile script. Many languages will be properly autodetected and compiled if needed without the need for an install.sh script.

    + +

    Your main file must be called MyBot. Your language is recognized using the file extension of your MyBot file. The appropriate file extensions for each language are:

    + +
      +
    • Java - .java
    • +
    • Python - .py
    • +
    • C++ - .cpp and .h(pp)
    • +
    • C# - .cs
    • +
    • Rust - .toml (for your Cargo.toml) and .rs (for your Rust source)
    • +
    • Scala - .scala
    • +
    • Ruby - .rb
    • +
    • Go - .go
    • +
    • PHP - .php
    • +
    • JavaScript - .js
    • +
    • OCaml - .ml
    • +
    • Clojure - .clj
    • +
    • C - .c
    • +
    + +

    See the Game Server Reference for details about compiler and runtime versions.

    + +

    Customizing your language name

    + +

    If you are using a language that is generic or that does not have first class support on the server, you can include a file named LANGUAGE containing the name of the language you are using. This will be used only for display on the rankings and in your profile.

    + +

    JVM Languages

    + +

    For JVM languages, you can submit a jar file inside of your zip file instead of source files. The jar will be executed java -jar MyBot.jar so you need to define a Main-Class header in the manifest.

    + +

    Running

    + +

    You may supply a run.sh script to control how your bot is run. Many languages will be properly autodetected and run without the need for an install.sh script. You should only include a custom run.sh script if you have a real need for one.

    + +

    Custom Runtime

    + +

    You could use a run.sh file to use a custom runtime such as PyPy instead of the default Python 3.

    + +

    Understanding Game Logs

    + +

    When your bot times out or errors on our game servers, we save and display a log file with debugging information including the time your bot took each turn, its output each turn, and its final output from stdout and stderr.

    + +

    To find these log files, visit your homepage. Just click the download log button to grab your error log for a game.

    + +
    +
    + +
    + + + + + + + + + + diff --git a/website/advanced_replay_file.php b/website/advanced_replay_file.php index 4be5b0a2..807d4654 100644 --- a/website/advanced_replay_file.php +++ b/website/advanced_replay_file.php @@ -2,7 +2,7 @@ - Replay File Reference + Replay Files @@ -14,7 +14,7 @@
    -

    Replay File Reference

    +

    Replay Files

    Overview

    diff --git a/website/guides_strategy.php b/website/advanced_strategy.php similarity index 100% rename from website/guides_strategy.php rename to website/advanced_strategy.php diff --git a/website/advanced_third_party.php b/website/advanced_third_party.php new file mode 100644 index 00000000..2d889d2d --- /dev/null +++ b/website/advanced_third_party.php @@ -0,0 +1,76 @@ + + + + + Third Party Resources + + + + + + + + + + + + + + diff --git a/website/api/API.class.php b/website/api/API.class.php index 5e9c54fc..54608c49 100644 --- a/website/api/API.class.php +++ b/website/api/API.class.php @@ -42,7 +42,7 @@ abstract class API{ protected $file = Null; protected function numRows($sql) { - return mysqli_query($this->mysqli, $sql)->num_rows; + return mysqli_fetch_assoc(mysqli_query($this->mysqli, $sql))["COUNT(*)"]; } protected function select($sql) { diff --git a/website/api/manager/ManagerAPI.php b/website/api/manager/ManagerAPI.php index c04894e5..828ce744 100644 --- a/website/api/manager/ManagerAPI.php +++ b/website/api/manager/ManagerAPI.php @@ -65,6 +65,13 @@ private function getTrueskillMatchQuality($rankingValues) { return floatval($lines[0]); } + private function checkConfig($section, $key) { + if(!isset($this->config[$section]) || !isset($this->config[$section][$key])) { + return false; + } + return $this->config[$section][$key]; + } + /////////////////////////API ENDPOINTS\\\\\\\\\\\\\\\\\\\\\\\\\\\\ @@ -81,22 +88,34 @@ protected function task() { ); } + // don't give out any task if games have been stopped. + if($this->checkConfig("compState", "noGameTasks")) { + return array("type" => "notask"); + } + // Assign a run game tasks $possibleNumPlayers = array(2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6); $numPlayers = $possibleNumPlayers[array_rand($possibleNumPlayers)]; $seedPlayer = null; - if((mt_rand() / mt_getrandmax()) > 0.5) { - $seedPlayer = $this->select("SELECT * FROM User WHERE isRunning = 1 order by rand()*-pow(sigma, 2) LIMIT 1"); + if(!$this->checkConfig("compState", "finalsPairing")) { + $randValue = mt_rand() / mt_getrandmax(); + if($randValue > 0.5) { + $seedPlayer = $this->select("SELECT * FROM User WHERE isRunning = 1 and numGames < 400 order by rand()*-pow(sigma, 2) LIMIT 1"); + } + if ($randValue > 0.25 && $randValue <= 0.5) { + $seedPlayer = $this->select("SELECT * FROM (SELECT u.* FROM (SELECT MAX(g.timestamp) as maxTime, gu.userID as userID FROM GameUser gu INNER JOIN Game g ON g.gameID=gu.gameID GROUP BY gu.userID) temptable INNER JOIN User u on u.userID = temptable.userID where numGames < 400 and isRunning = 1 order by maxTime ASC limit 15) orderedTable order by rand() limit 1;"); + } + if($randValue <= 0.25 || count($seedPlayer) < 1) { + $seedPlayer = $this->select("SELECT u.* FROM (SELECT MAX(g.timestamp) as maxTime, gu.userID as userID FROM GameUser gu INNER JOIN Game g ON g.gameID=gu.gameID GROUP BY gu.userID) temptable INNER JOIN User u on u.userID = temptable.userID where isRunning = 1 order by maxTime ASC limit 1"); + } } else { - $seedPlayer = $this->select("SELECT * FROM (SELECT u.* FROM (SELECT MAX(g.timestamp) as maxTime, gu.userID as userID FROM GameUser gu INNER JOIN Game g ON g.gameID=gu.gameID GROUP BY gu.userID) temptable INNER JOIN User u on u.userID = temptable.userID where numGames < 400 order by maxTime ASC limit 15) orderedTable order by rand() limit 1;"); - if(count($seedPlayer) < 1) $seedPlayer = $this->select("SELECT u.* FROM (SELECT MAX(g.timestamp) as maxTime, gu.userID as userID FROM GameUser gu INNER JOIN Game g ON g.gameID=gu.gameID GROUP BY gu.userID) temptable INNER JOIN User u on u.userID = temptable.userID order by maxTime ASC limit 1"); + $seedPlayer = $this->select("SELECT * FROM (SELECT * FROM User WHERE rank < 398 and isRunning = 1 ORDER BY numGames ASC limit 15) orderedTable ORDER BY rand() LIMIT 1"); } - if(count($seedPlayer) < 1) return null; $muRankLimit = intval(5.0 / pow((float)mt_rand(1, mt_getrandmax())/(float)mt_getrandmax(), 0.65)); $players = $this->selectMultiple("SELECT * FROM (SELECT * FROM User WHERE isRunning=1 and userID <> {$seedPlayer['userID']} ORDER BY ABS(mu-{$seedPlayer['mu']}) LIMIT $muRankLimit) muRankTable ORDER BY rand() LIMIT ".($numPlayers-1)); - array_push($players, $seedPlayer); + array_unshift($players, $seedPlayer); // Pick map size $sizes = array(20, 25, 25, 30, 30, 30, 35, 35, 35, 35, 40, 40, 40, 45, 45, 50); @@ -148,19 +167,20 @@ protected function game() { // Each user in users must have a rank, playerIndex, mu, sigma, and userID if(isset($_POST['users']) && count($_FILES) > 0) { $this->insert("UPDATE Worker SET numGames=numGames+1 WHERE apiKey=".$this->mysqli->real_escape_string($this->apiKey)); + $workerID = $this->select("SELECT workerID FROM Worker WHERE apiKey=".$this->mysqli->real_escape_string($this->apiKey))["workerID"]; $mapWidth = $_POST['mapWidth']; $mapHeight = $_POST['mapHeight']; - $users = json_decode($_POST['users']); - $storedUsers = array(); + $users = json_decode($_POST['users'], true); // Throw out the game if it is using an old version of a person's bot - foreach($users as $user) { - $storedUser = $this->select("SELECT * FROM User WHERE userID=".$this->mysqli->real_escape_string($user->userID)); - array_push($storedUsers, $storedUser); - if(intval($storedUser['numSubmissions']) != intval($user->numSubmissions)) { + foreach($users as $key => $user) { + // Will need email credentials for email sending, numSubmissions for version checking, and mu + sigma so we can update trueskill + $storedUser = $this->select("SELECT userID, onEmailList, email, numSubmissions, mu, sigma FROM User WHERE userID=".$this->mysqli->real_escape_string($user['userID'])); + if(intval($storedUser['numSubmissions']) != intval($user['numSubmissions'])) { return null; } + $users[$key] = array_merge($user, $storedUser); // Second param overwrites first param } // Store replay file and error logs @@ -185,11 +205,9 @@ protected function game() { $s3Client->putObject($args); } - // Check that we arent stoing too many games in db - $numAllowed = 100000; - $res = mysqli_query($this->mysqli, "SELECT * FROM Game"); - $numRows = $res->num_rows; + $numAllowed = 600000; + $numRows = $this->numRows("SELECT COUNT(*) FROM Game"); $numToDelete = $numRows - $numAllowed; if($numToDelete > 0) { $gamesToDelete = $this->selectMultiple("SELECT gameID FROM Game ORDER BY gameID LIMIT $numToDelete"); @@ -200,53 +218,51 @@ protected function game() { } // Store game information in db - $this->insert("INSERT INTO Game (replayName, mapWidth, mapHeight, timestamp) VALUES ('".$this->mysqli->real_escape_string($replayName)."', ".$this->mysqli->real_escape_string($mapWidth).", ".$this->mysqli->real_escape_string($mapHeight).", NOW())"); - $gameIDArray = $this->select("SELECT gameID FROM Game WHERE replayName = '".$this->mysqli->real_escape_string($replayName)."' LIMIT 1"); - $gameID = $gameIDArray['gameID']; + $this->insert("INSERT INTO Game (replayName, mapWidth, mapHeight, timestamp, workerID) VALUES ('".$this->mysqli->real_escape_string($replayName)."', ".$this->mysqli->real_escape_string($mapWidth).", ".$this->mysqli->real_escape_string($mapHeight).", NOW(), $workerID)"); + $gameID = $this->mysqli->insert_id; // Update each participant's stats for($a = 0; $a < count($users); $a++) { - $timeoutInt = $users[$a]->didTimeout ? 1 : 0; - $errorLogName = $users[$a]->errorLogName == NULL ? "NULL" : "'".$this->mysqli->real_escape_string($users[$a]->errorLogName)."'"; - $this->insert("INSERT INTO GameUser (gameID, userID, errorLogName, rank, playerIndex, didTimeout, versionNumber) VALUES ($gameID, ".$this->mysqli->real_escape_string($users[$a]->userID).", $errorLogName, ".$this->mysqli->real_escape_string($users[$a]->rank).", ".$this->mysqli->real_escape_string($users[$a]->playerTag).", {$timeoutInt}, {$storedUsers[$a]['numSubmissions']})"); + $timeoutInt = $users[$a]['didTimeout'] ? 1 : 0; + $errorLogName = $users[$a]['errorLogName'] == NULL ? "NULL" : "'".$this->mysqli->real_escape_string($users[$a]['errorLogName'])."'"; + $this->insert("INSERT INTO GameUser (gameID, userID, errorLogName, rank, playerIndex, didTimeout, versionNumber) VALUES ($gameID, ".$this->mysqli->real_escape_string($users[$a]['userID']).", $errorLogName, ".$this->mysqli->real_escape_string($users[$a]['rank']).", ".$this->mysqli->real_escape_string($users[$a]['playerTag']).", {$timeoutInt}, {$users[$a]['numSubmissions']})"); // Increment number of games - $this->insert("UPDATE User SET numGames=numGames+1 WHERE userID=".$users[$a]->userID); + $this->insert("UPDATE User SET numGames=numGames+1 WHERE userID=".$users[$a]['userID']); } // Send first game email and first timeout email foreach($users as $user) { - $storedUser = $this->select("SELECT * FROM User WHERE userID=".$user->userID); - if($user->didTimeout && mysqli_query($this->mysqli, "SELECT * from GameUser WHERE didTimeout = 1 and versionNumber = {$storedUser['numSubmissions']} and userID={$user->userID}")->num_rows == 1) { + if($user['didTimeout'] && mysqli_query($this->mysqli, "SELECT * from GameUser WHERE didTimeout = 1 and versionNumber = {$user['numSubmissions']} and userID={$user['userID']}")->num_rows == 1) { $errorLogContents = NULL; foreach($_FILES as $file) { - if($file['name'] == $user->errorLogName) { + if($file['name'] == $user['errorLogName']) { $errorLogContents = file_get_contents($file['tmp_name']); break; } } if($errorLogContents == NULL) continue; - $message = "

    Your bot timed out in a game for the first time. This happens when your bot doesn't respond in 15 seconds of a game's start or 1 second of a turn's start. A timeout may be the result of a runtime error in your bot. When your bot times out, its pieces become part of the map and it is ejected from the game.

    Here is a visualization of the game in which your bot timed out.

    Here is your bot's error log. An error log contains your bot's output (from stdout and stderr) and the time it took per turn. For more on error logs, see the dev guide.

    "; - $this->sendNotification($storedUser, "First Bot Timeout/Error", $message, -1); + $message = "

    Your bot timed out in a game for the first time. This happens when your bot doesn't respond in 15 seconds of a game's start or 1 second of a turn's start. A timeout may be the result of a runtime error in your bot. When your bot times out, its pieces become part of the map and it is ejected from the game.

    Here is a visualization of the game in which your bot timed out.

    Here is your bot's error log. An error log contains your bot's output (from stdout and stderr) and the time it took per turn.

    "; + $this->sendNotification($user, "First Bot Timeout/Error", $message, -1); } } // Update mu and sigma for the players usort($users, function($a, $b) { - return $a->rank > $b->rank; + return $a['rank'] > $b['rank']; }); $rankings = array(); foreach($users as $user) { - array_push($rankings, $user->mu); - array_push($rankings, $user->sigma); + array_push($rankings, $user['mu']); + array_push($rankings, $user['sigma']); } exec("python3 updateTrueskill.py ".implode(' ', $rankings), $lines); var_dump($lines); for($a = 0; $a < count($users); $a++) { $components = explode(' ', $lines[$a]); - $this->insert("UPDATE User SET mu=".$this->mysqli->real_escape_string($components[0]).", sigma=".$this->mysqli->real_escape_string($components[1])." WHERE userID=".$this->mysqli->real_escape_string($users[$a]->userID)); + $this->insert("UPDATE User SET mu=".$this->mysqli->real_escape_string($components[0]).", sigma=".$this->mysqli->real_escape_string($components[1])." WHERE userID=".$this->mysqli->real_escape_string($users[$a]['userID'])." AND numSubmissions=".$this->mysqli->real_escape_string($users[$a]['numSubmissions'])); } // Update overall rank of everyone @@ -254,10 +270,13 @@ protected function game() { usort($allUsers, function($a, $b) { return $a['mu']-3*$a['sigma'] < $b['mu']-3*$b['sigma']; }); + $query = "UPDATE User set rank = CASE"; for($userIndex = 0; $userIndex < count($allUsers); $userIndex++) { $rank = $userIndex+1; - $this->insert("UPDATE User SET rank={$rank} WHERE userID={$allUsers[$userIndex]['userID']}"); + $query .= " WHEN userID = {$allUsers[$userIndex]['userID']} THEN {$rank}"; } + $query .= " ELSE rank END;"; + $this->insert($query); } } diff --git a/website/api/web/WebsiteAPI.php b/website/api/web/WebsiteAPI.php index 250cfc62..e51cf919 100644 --- a/website/api/web/WebsiteAPI.php +++ b/website/api/web/WebsiteAPI.php @@ -18,7 +18,7 @@ include dirname(__FILE__).'/../API.class.php'; define("ORGANIZATION_WHITELIST_PATH", dirname(__FILE__)."/../../organizationWhitelist.txt"); -define("USER_TO_SERVER_RATIO", 30); +define("USER_TO_SERVER_RATIO", 100); define("WORKER_LIMIT", 50); class WebsiteAPI extends API{ @@ -69,6 +69,7 @@ private function isLoggedIn() { private function getUsers($query, $privateInfo=false) { $users = $this->selectMultiple($query); + $numUsers = $this->numRows("SELECT COUNT(*) FROM User WHERE isRunning=1"); foreach($users as &$user) { if($privateInfo == false) { unset($user['email']); @@ -77,7 +78,7 @@ private function getUsers($query, $privateInfo=false) { } if(intval($user['isRunning']) == 1) { - $percentile = intval($user['rank']) / $this->numRows("SELECT * FROM User WHERE isRunning=1"); + $percentile = intval($user['rank']) / $numUsers; if($percentile < 1/64) $user['tier'] = "Diamond"; else if($percentile < 1/16) $user['tier'] = "Gold"; else if($percentile < 1/4) $user['tier'] = "Silver"; @@ -94,6 +95,20 @@ private function getLoggedInUser() { if(isset($_SESSION['userID'])) return $this->getUsers("SELECT * FROM User WHERE userID={$_SESSION['userID']}", true)[0]; } + /** + * Helper to interface with the database and get high schools based on the filters. + */ + private function getHS($name=null, $state=null) { + $query_string = "SELECT * FROM HighSchool "; + if(!empty($name) && isset($name)) { + $query_string = $query_string."WHERE name='".$this->mysqli->real_escape_string($name)."' "; + } + if(!empty($state) && isset($state)) { + $query_string = $query_string.(!empty($name) && isset($name)?"AND":"WHERE")." state='".$this->mysqli->real_escape_string($state)."' "; + } + return $this->selectMultiple($query_string."ORDER BY name ASC"); + } + private function getOrganizationForEmail($email) { $emailDomain = explode('@', $email)[1]; $rows = explode("\n", rtrim(file_get_contents(ORGANIZATION_WHITELIST_PATH))); @@ -163,11 +178,11 @@ protected function user() { $githubUser = json_decode($gitHub->request('user'), true); $email = json_decode($gitHub->request('user/emails'), true)[0]; - if($this->numRows("SELECT userID FROM User WHERE oauthProvider=1 and oauthID={$githubUser['id']}") > 0) { // Already signed up + if($this->numRows("SELECT COUNT(*) FROM User WHERE oauthProvider=1 and oauthID={$githubUser['id']}") > 0) { // Already signed up $_SESSION['userID'] = $this->select("SELECT userID FROM User WHERE oauthProvider=1 and oauthID={$githubUser['id']}")['userID']; } else { // New User - $numActiveUsers = $this->numRows("SELECT userID FROM User WHERE isRunning=1"); + $numActiveUsers = $this->numRows("SELECT COUNT(*) FROM User WHERE isRunning=1"); $this->insert("INSERT INTO User (username, githubEmail, oauthID, oauthProvider, rank) VALUES ('{$githubUser['login']}', '{$email}', {$githubUser['id']}, 1, {$numActiveUsers})"); $_SESSION['userID'] = $this->mysqli->insert_id; } @@ -190,9 +205,27 @@ protected function email() { $this->insert("UPDATE User SET email=githubEmail, organization='$organization', isEmailGood=1 WHERE userID = {$user['userID']}"); } else if($user != null && isset($_GET['newEmail'])) { $verificationCode = rand(0, 9999999999); - $this->insert("UPDATE User SET email='".$this->mysqli->real_escape_string($_GET['newEmail'])."', verificationCode = '{$verificationCode}' WHERE userID = {$user['userID']}"); - $user["email"] = $_GET["newEmail"]; + if(isset($_GET['newLevel']) && $_GET['newLevel'] == 'High School') { + if(empty($this->getHS($_GET['newInstitution'], null))) { + # The only way this error should occur is if users manually try to game it (i.e.: REST calls) + # As such we can just print their input is incorrect rather than getting a better landing page. + echo "INVALID INPUT: EITHER INSTITUTION OR SCRIMMAGE ARE NOT FROM AVAILABLE OPTIONS."; + die(); + } + $this->insert("UPDATE User SET email='".$this->mysqli->real_escape_string($_GET['newEmail']). + "', level='".$this->mysqli->real_escape_string($_GET['newLevel']). + "', organization='".$this->mysqli->real_escape_string($_GET['newInstitution']). + "', verificationCode = '{$verificationCode}' WHERE userID = {$user['userID']}"); + } else if(isset($_GET['newLevel'])) { + $this->insert("UPDATE User SET email='".$this->mysqli->real_escape_string($_GET['newEmail']). + "', level='".$this->mysqli->real_escape_string($_GET['newLevel']). + "', organization='".$this->getOrganizationForEmail($this->mysqli->real_escape_string($_GET['newEmail'])). + "', verificationCode = '{$verificationCode}' WHERE userID = {$user['userID']}"); + } else { + $this->insert("UPDATE User SET email='".$this->mysqli->real_escape_string($_GET['newEmail'])."', verificationCode = '{$verificationCode}' WHERE userID = {$user['userID']}"); + } + $user["email"] = $_GET["newEmail"]; $this->sendNotification($user, "Email Verification", "

    Click here to verify your email address.

    ", 0, false, true); } else if(isset($_GET['verificationCode'])) { if($user == null) { @@ -249,6 +282,14 @@ protected function history() { } } + /* High School Endpoint + * + * Simple retrieval from the high school store. All participating high schools should be here. + */ + protected function highSchool() { + return $this->getHS(isset($_GET["name"])?$_GET["name"]:null, isset($_GET["state"])?$_GET["state"]:null); + } + /* User Notification Endpoint * * Allows the downloading of all of the notifications a user has recieved over email. @@ -274,16 +315,20 @@ protected function game() { $versionNumber = isset($_GET['versionNumber']) ? intval($_GET['versionNumber']) : $this->select("SELECT numSubmissions FROM User WHERE userID=$userID")['numSubmissions']; $gameArrays = $this->selectMultiple("SELECT g.* FROM GameUser gu INNER JOIN Game g ON g.gameID = gu.gameID WHERE gu.userID = $userID and gu.versionNumber = $versionNumber and gu.gameID < $startingID ORDER BY gu.gameID DESC LIMIT $limit"); + } else { + $previousID = isset($_GET['previousID']) ? intval($_GET['previousID']) : 0; + $limit = isset($_GET['limit']) ? intval($_GET['limit']) : 20; + $gameArrays = $this->selectMultiple("SELECT * FROM Game WHERE gameID > $previousID ORDER BY gameID DESC LIMIT $limit"); + } - // Get each game's info - foreach ($gameArrays as &$gameArray) { - $gameID = $gameArray['gameID']; + // Get each game's info + foreach ($gameArrays as &$gameArray) { + $gameID = $gameArray['gameID']; - // Get information about users - $gameArray['users'] = $this->selectMultiple("SELECT gu.userID, gu.errorLogName, gu.rank, u.username, u.oauthID FROM GameUser gu INNER JOIN User u ON u.userID=gu.userID WHERE gu.gameID = $gameID"); - } - return $gameArrays; - } + // Get information about users + $gameArray['users'] = $this->selectMultiple("SELECT gu.userID, gu.versionNumber, gu.errorLogName, gu.rank, u.username, u.oauthID, u.mu, u.sigma, u.rank AS userRank FROM GameUser gu INNER JOIN User u ON u.userID=gu.userID WHERE gu.gameID = $gameID"); + } + return $gameArrays; } /* Bot File Endpoint @@ -294,6 +339,10 @@ protected function botFile() { // Mark a new botfile for compilation if valid. Return error otherwise if($this->isLoggedIn() && isset($_FILES['botFile']['name'])) { $user = $this->getLoggedInUser(); + + if(isset($this->config["compState"]["closeSubmissions"]) && $this->config["compState"]["closeSubmissions"]) { + return "Sorry, bot submissions are closed."; + } if($user['compileStatus'] != 0) { return "Compiling"; @@ -315,8 +364,8 @@ protected function botFile() { if(intval($this->config['test']['isTest']) == 0) $this->sendNotification($user, "Bot Received", "

    We have received and processed the zip file of your bot's source code. In a few minutes, our servers will compile your bot, and you will receive another email notification, even if your bot has compilation errors.

    ", 0); // AWS auto scaling - $numActiveUsers = $this->numRows("SELECT userID FROM User WHERE isRunning=1"); - $numWorkers = $this->numRows("SELECT workerID FROM Worker"); + $numActiveUsers = $this->numRows("SELECT COUNT(*) FROM User WHERE isRunning=1"); + $numWorkers = $this->numRows("SELECT COUNT(*) FROM Worker"); if($numWorkers > 0 && $numWorkers < WORKER_LIMIT && $numActiveUsers / $numWorkers > USER_TO_SERVER_RATIO) { echo shell_exec("python3 openNewWorker.py > /dev/null 2>/dev/null &"); } @@ -403,6 +452,23 @@ protected function stats() { else if(isset($_GET['numActive'])) { return mysqli_query($this->mysqli, "SELECT userID FROM User WHERE isRunning=1")->num_rows; } + + // Get the median mu and sigma of active users + else if(isset($_GET['scoreMedians'])) { + $medians = array(); + $medians["mu"] = $this->select("SELECT AVG(tbl.mu) as medianMu FROM ( + SELECT @rownum:=@rownum+1 as row, mu FROM User, (SELECT @rownum:=0) rn + WHERE isRunning=1 ORDER BY mu + ) as tbl + WHERE tbl.row in (floor((@rownum+1)/2), floor((@rownum+2)/2))")["medianMu"]; + + $medians["sigma"] = $this->select("SELECT AVG(tbl.sigma) as medianSigma FROM ( + SELECT @rownum:=@rownum+1 as row, sigma FROM User, (SELECT @rownum:=0) rn + WHERE isRunning=1 ORDER BY sigma + ) as tbl + WHERE tbl.row in (floor((@rownum+1)/2), floor((@rownum+2)/2))")["medianSigma"]; + return $medians; + } } /* Announcement Endpoint diff --git a/website/archiveStarterPackages.sh b/website/archiveStarterPackages.sh index 981d8c7a..3fc5ce72 100755 --- a/website/archiveStarterPackages.sh +++ b/website/archiveStarterPackages.sh @@ -15,7 +15,9 @@ mkdir Halite-Python-Starter-Package \ Halite-PHP-Starter-Package \ Halite-JavaScript-Starter-Package \ Halite-OCaml-Starter-Package \ - Halite-Clojure-Starter-Package + Halite-Clojure-Starter-Package \ + Halite-C-Starter-Package \ + Halite-Julia-Starter-Package cp -r Python/* Halite-Python-Starter-Package/ cp -r Java/* Halite-Java-Starter-Package/ @@ -29,6 +31,8 @@ cp -r PHP/* Halite-PHP-Starter-Package/ cp -r JavaScript/* Halite-JavaScript-Starter-Package/ cp -r OCaml/* Halite-OCaml-Starter-Package/ cp -r Clojure/* Halite-Clojure-Starter-Package/ +cp -r C/* Halite-C-Starter-Package/ +cp -r Julia/* Halite-Julia-Starter-Package/ cp -r Scala/* Halite-Scala-Starter-Package/ rm Halite-Scala-Starter-Package/MyBot.java @@ -45,6 +49,8 @@ zip -r Halite-PHP-Starter-Package.zip Halite-PHP-Starter-Package/ zip -r Halite-JavaScript-Starter-Package.zip Halite-JavaScript-Starter-Package/ zip -r Halite-OCaml-Starter-Package.zip Halite-OCaml-Starter-Package/ zip -r Halite-Clojure-Starter-Package.zip Halite-Clojure-Starter-Package/ +zip -r Halite-C-Starter-Package.zip Halite-C-Starter-Package/ +zip -r Halite-Julia-Starter-Package.zip Halite-Julia-Starter-Package/ mkdir -p ../website/downloads/starterpackages mv *.zip ../website/downloads/starterpackages diff --git a/website/associate.php b/website/associate.php new file mode 100644 index 00000000..cfa4f050 --- /dev/null +++ b/website/associate.php @@ -0,0 +1,73 @@ + + + + + + + Association + + + + + + + +
    +
    +
    +
    +
    +
    +

    Share Your Affiliation

    +

    Curious to see how your organization stacks up against the others? Want to see who else from your company or school is playing Halite?

    +

    Enter your work/school information below and we'll match you with your organization. This way you can see your ranking on your team's leaderboard as well as your ranking overall. May the best bot win!

    +
    + + +
    + + +
    + +
    + + +
    + +
    + + + + +
    + +
    +
    +
    +

    +
    +

    Is your organization missing from our selection? Click here to e-mail us with the name and we will add it promptly.

    +
    + +
    + + + + + + + + diff --git a/website/basics_improve_random.php b/website/basics_improve_random.php index 3babeadb..070e8859 100644 --- a/website/basics_improve_random.php +++ b/website/basics_improve_random.php @@ -17,7 +17,7 @@

    Improving the Random Bot

    In this tutorial, we'll go through the code that powers the random bot and add a couple heuristics to it. This will hopefully help you fully understand Halite and set you on your way to leaderboard domination.

    -

    The code in this tutorial can be found at the following links for Python, Java, and C++.

    +

    The code in this tutorial can be found at the following links for Python, Java, and C++.

    Prerequisites

    Make sure that you have read Introducing Halite and followed the setup procedures described there.

    Now open up the MyBot file in your favorite editor and let's get started!

    @@ -27,28 +27,30 @@

    A Look At the Random Bot

    Now that you know how the game works, how do the two random starter bots work? How does one code a Halite bot? Here is the source from the main file of our python starter bot:

    - +

    Let's walk through it line by line.

    -

    First we import a couple of helper files that are included in the starter packages:

    +

    First we make some imports from the hlt.py helper file that is included in the starter package:

    - + -

    Then we get our ID (each player has a unique identifier that is associated with their pieces) and the game initial map from the environment.

    -

    We send back the name of our bot. This is used in game replays.

    +

    Then we get our ID (each player has a unique identifier that is associated with their pieces) and the initial game map from the environment. We send back the name of our bot. This is used in game replays.

    - + -

    Now we start our game loop. Each frame let's initialize a list of moves and get the current map:

    - +

    Now we start our game loop:

    + -

    Let's cycle through all of the pieces on the map. If a piece is owned by us, let's order it to move in a random direction.

    +

    Each turn, update the current map from the game environment:

    + - +

    Let's create our list of moves by cycling through all the pieces on the map. If a piece is owned by us, let's instruct it to move in a random direction.

    + +

    Finally, let's send all of our moves to the environment:

    - +

    And that's random bot!

    @@ -59,26 +61,27 @@
  • A zero strength piece that moves will necessarily stay at zero strength, because pieces don't gain strength for any turn that they move.
  • A zero strength piece won't ever conqueror any territory, because it has no strength with which to damage other pieces.
  • -

    Let's wrap the movement logic inside a function of its own. This function will take the location of a piece and will return the piece's movement.

    +

    Let's wrap the movement logic inside a function of its own. This function will take a piece as input and will return the piece's movement.

    Now we can improve our bot by making sure that we tell all of our zero strength pieces to remain still.

    - +

    Our bot still moves its pieces around a lot (only a bit over one out of five turns will a piece stay still). This costs us a lot of strength (since a piece doesn't gain any strength on turns that it moves). To increase our utilization of our production, let's have pieces only move once their strength equals their production times some factor X. We're using 5 as the value of X in this example, but this is arbitrary.

    - +

    Moving to Our Borders

    When building a Halite bot, one of our goals should be moving strength out of your territory quickly and with little production loss. Our current bot is terrible at this. Its pieces move randomly around our territory, going nowhere, costing us production, and often losing strength to the strength cap.

    To improve this, let's just mandate that our pieces move only north and west. Since the map is wrap-around, we can still capture all of the board with this strategy!

    - +

    Improving our Attack

    Once our pieces get to our borders, we don't want them to randomly attack just any square (or worse, move back into our territory), as we do now. One problem with this random strategy is that we may attack a map square that has more strength than us. This is unproductive (pun implied) since moving onto the map square costs us a turn of production and we don't actually gain anything. We just diminish the squares strength.

    To improve on our current combat, if there is an enemy or map square that is adjacent to one of our pieces with less strength than our piece, let's take it.

    - + -

    What Next?

    +

    What's Next?

    That's really up to you! How you improve your bot from here is where you step into the competition.

    -

    That said, if you're looking for more ideas or a stronger starting base, nmlaguti wrote a tutorial here that we highly recommend.

    +

    That said, if you're looking for more ideas or a stronger starting base, nmalaguti wrote a tutorial that walks you through improving your combat, piece management, and expansion.

    +

    If you're interested in machine learning, Two Sigma's own Brian van Leeuwen authored an introductory deep learning tutorial.

    Good luck!

    diff --git a/website/basics_intro_halite.php b/website/basics_intro_halite.php index 6b8ab89b..701652a3 100644 --- a/website/basics_intro_halite.php +++ b/website/basics_intro_halite.php @@ -31,10 +31,10 @@

    Testing your bot

    To play games of Halite locally, you will need the game environment. As of this writing we support Windows, Linux and OSX platforms. You can download the game environment here. Place the downloaded binary (halite or halite.exe) in your starter kit folder.

    -

    To simulate a game, simply runGame.sh (Linux and macOS) or runGame.bat (Windows). This command will run a game between my MyBot and RandomBot (both are just copies of each other at this point) on a grid of size 30x30.

    +

    To simulate a game, simply issue the command ./runGame.sh (Linux and macOS) or runGame.bat (Windows). This command will run a game between my MyBot and RandomBot (both are just copies of each other at this point) on a grid of size 30x30.

    The output should look like this and the details of the game will be stored in a file with the "hlt" extension (35538-124984302.hlt in the example below).

    -
    $ . runGame.sh 
    +                
    $ ./runGame.sh 
     python3 MyBot.py
     python3 RandomBot.py
     Init Message sent to player 2.
    @@ -55,8 +55,10 @@
     
     
                     

    Visualizing a game

    -

    The console output from the game environment gives just the outcome of the game. To replay the game, drag and drop the file to the visualizer to get a visualization like this one:

    -
    +

    The console output from the game environment gives just the outcome of the game. To replay the game, drag and drop the file to the visualizer. Since the starter pack is very bad at playing Halite, your visualization will be quite dull.

    +

    Here's a taste of what some very good Halite bots look like:

    +
    +

    Halite game rules

    What do all of these pretty squares mean?

    @@ -65,6 +67,7 @@

    When two or more pieces from the same player try to occupy the same site, the resultant piece gets the sum of their strengths (this strength is capped at 255).

    When pieces with different owners move onto the same site or cardinally adjacent sites, the pieces are forced to fight, and each piece loses strength equal to the strength of its opponent. When a player's piece moves onto an unowned site, that piece and the unowned piece fight, and each piece loses strength equal to the strength of its opponent.

    When a piece loses all of its strength, it dies and is removed from the grid.

    +

    For the full rules, see here.

    How do we program a bot?

    Move on to Improving the Random Bot.

    @@ -75,8 +78,14 @@ + + + + + + diff --git a/website/basics_quickstart.php b/website/basics_quickstart.php index 68520512..5937d325 100644 --- a/website/basics_quickstart.php +++ b/website/basics_quickstart.php @@ -38,8 +38,9 @@

    Congratulations. You are now an official contestant!

    It may be up to 10 minutes before your bot has played a few games. In the meantime, move onto Introducing Halite.

    -

    Need Help? Have Feedback? Want to Talk?

    -

    Please post on the forums. For more unstructured conversations, join our discord.

    +

    Join the Community

    +

    If you need help or have feedback, please visit the forums! If you’d like to hang out and chat with other players, come join the official discord.

    +
    diff --git a/website/downloads.php b/website/downloads.php index 2425cc54..392e7800 100644 --- a/website/downloads.php +++ b/website/downloads.php @@ -16,7 +16,7 @@

    Starter Packages

    - This set of starter packages was uploaded on November 13th. We uploaded a Go starter package. + On December 31st, we patched the version of the Java starter package that we had released on December 30th.

    Official Packages

    Game Environment

    -

    The environment is on version 1.0. This version of the environment was posted on November 2nd.

    +

    The environment is on version 1.1. This version of the environment was posted on December 9th.

    Linux/macOS

    Execute:

    @@ -47,7 +49,7 @@

    You should see a file titled "halite" in your current directory. This is the game environment. Put it in the root directory of your starter package.

    Windows

    -

    Download halite.exe. Put it in the root directory of your starter package.

    +

    Download the halite.exe executable from here. Put it in the root directory of your starter package.

    Building from Source

    Extract this archive.

    diff --git a/website/email.php b/website/email.php index 3768fb39..f1aaa94f 100644 --- a/website/email.php +++ b/website/email.php @@ -44,7 +44,6 @@
    -
    diff --git a/website/game.php b/website/game.php index 5c1be3c4..052542c1 100644 --- a/website/game.php +++ b/website/game.php @@ -7,6 +7,29 @@ + +
    @@ -15,6 +38,7 @@
    + diff --git a/website/guides_libraries.php b/website/guides_libraries.php deleted file mode 100644 index 8a3892d5..00000000 --- a/website/guides_libraries.php +++ /dev/null @@ -1,51 +0,0 @@ - - - - - Using 3rd Party Libraries - - - - - - -
    - -
    - -
    -

    Using 3rd Party Libraries

    -

    If you add a bash file named install.sh to the same directory as your bot source, it will be run with internet access and write access to its current directory before any bot compilation. It may run for a maximum of 10 minutes.

    - -

    Package Managers

    -

    You must install all libraries locally, even if through a package manager. Because of this, package managers that only support global installation (such as apt-get) may not be used.

    -

    The following package managers are installed ahead of time on the Halite game servers:

    -
      -
    • pip3
    • -
    • bundler
    • -
    • npm
    • -
    -

    - If you would like us to add another package manager to this list, post on the forums. -

    - -

    Here is an example install.sh file for a bot that uses the numpy library:

    - -

    It's just one line!

    - -

    Compilation

    -

    If your library isn't on a package manager that supports local installation, you are going to have to compile it on our game servers. Include the source of you library in your bot's zip file and put compilation instructions in the install.sh file.

    -
    -
    - -
    - - - - - - - - - - diff --git a/website/includes/leaderTable.php b/website/includes/leaderTable.php index e71fcbe6..f0cd1e6f 100644 --- a/website/includes/leaderTable.php +++ b/website/includes/leaderTable.php @@ -5,6 +5,7 @@ User Tier Language + Level Organization Points diff --git a/website/includes/learn_sidebar.php b/website/includes/learn_sidebar.php index 5e945e42..2f446191 100644 --- a/website/includes/learn_sidebar.php +++ b/website/includes/learn_sidebar.php @@ -46,7 +46,7 @@