Skip to content

Commit 9d817ad

Browse files
authored
Merge pull request #152 from stoianchoo/master
TypeScript starter kit
2 parents db81c9d + 95b9360 commit 9d817ad

24 files changed

+1302
-0
lines changed

starter_kits/TypeScript/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.hlt
2+
*.log
3+
*~ts
4+
/out/*

starter_kits/TypeScript/MyBot.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Constants } from "./hlt/Constants";
2+
import { Direction } from "./hlt/Direction";
3+
import { Game } from "./hlt/Game";
4+
import { Logging } from "./hlt/Logging";
5+
import { Random } from "./hlt/Random";
6+
7+
const firstArgument = process.argv[2];
8+
const random = new Random(firstArgument ? Number(firstArgument) : 324231);
9+
10+
const game = new Game();
11+
game.initialize().then(async ([gameMap, me]) => {
12+
// At this point "game" variable is populated with initial map data.
13+
// This is a good place to do computationally expensive start-up pre-processing.
14+
// As soon as you call "ready" function below, the 2 second per turn timer will start.
15+
await game.ready("MyTypeScriptBot");
16+
17+
Logging.info(`My Player ID is ${game.myId}.`);
18+
19+
while (true) {
20+
await game.updateFrame();
21+
22+
const commandQueue = [];
23+
24+
// Movement algorithm parameters
25+
const fullCargoCoefficient = 0.5;
26+
const tooLittleHaliteToKeepHarvestingCoefficient = 0.1;
27+
const spawnNewShipsUntilGameTurnCoefficient = 0.75;
28+
29+
for (const ship of me.getShips()) {
30+
if (ship.haliteAmount > fullCargoCoefficient * Constants.MAX_ENERGY) {
31+
// Go home if full
32+
const destination = me.shipyard.position;
33+
const safeMove = gameMap.naiveNavigate(ship, destination);
34+
commandQueue.push(ship.move(safeMove));
35+
} else if (
36+
gameMap.get(ship.position).haliteAmount <
37+
tooLittleHaliteToKeepHarvestingCoefficient * Constants.MAX_ENERGY
38+
) {
39+
// Go harvest to a new place
40+
const getRandomArrayElement = (anArray: any[]) => anArray[Math.floor(anArray.length * random.next())];
41+
const direction = getRandomArrayElement(Direction.getAllCardinals());
42+
const destination = ship.position.directionalOffset(direction);
43+
const safeMove = gameMap.naiveNavigate(ship, destination);
44+
commandQueue.push(ship.move(safeMove));
45+
} else {
46+
// Keep still and harvest in place
47+
}
48+
}
49+
50+
if (game.turnNumber < spawnNewShipsUntilGameTurnCoefficient * Constants.MAX_TURNS &&
51+
// Spawn new ships
52+
me.haliteAmount >= Constants.SHIP_COST &&
53+
!gameMap.get(me.shipyard).isOccupied) {
54+
commandQueue.push(me.shipyard.spawn());
55+
}
56+
57+
await game.endTurn(commandQueue);
58+
}
59+
});

starter_kits/TypeScript/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Setup
2+
Run `npm install` to locally install the required dependencies before developing.
3+
4+
# Development
5+
The `run_game` scripts will run `tslint` and `tsc` to proof and compile the typescript sources before running the bot.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Commands } from "./Commands";
2+
3+
export class Direction {
4+
public static North = new Direction(0, -1);
5+
public static South = new Direction(0, 1);
6+
public static East = new Direction(1, 0);
7+
public static West = new Direction(-1, 0);
8+
public static Still = new Direction(0, 0);
9+
10+
public static getAllCardinals() {
11+
return [ Direction.North, Direction.South, Direction.East, Direction.West ];
12+
}
13+
14+
constructor(public dx: number, public dy: number) {
15+
this.dx = dx;
16+
this.dy = dy;
17+
}
18+
19+
public equals(other: Direction) {
20+
return this.dx === other.dx && this.dy === other.dy;
21+
}
22+
23+
public toString() {
24+
return `${this.constructor.name}(${this.dx}, ${this.dy})`;
25+
}
26+
27+
public toWireFormat() {
28+
if (this.equals(Direction.North)) {
29+
return Commands.NORTH;
30+
} else if (this.equals(Direction.South)) {
31+
return Commands.SOUTH;
32+
} else if (this.equals(Direction.East)) {
33+
return Commands.EAST;
34+
} else if (this.equals(Direction.West)) {
35+
return Commands.WEST;
36+
} else if (this.equals(Direction.Still)) {
37+
return Commands.STAY_STILL;
38+
}
39+
throw new Error(`Non-cardinal direction cannot be converted to wire format: ${this}`);
40+
}
41+
42+
public invert() {
43+
if (this.equals(Direction.North)) {
44+
return Direction.South;
45+
} else if (this.equals(Direction.South)) {
46+
return Direction.North;
47+
} else if (this.equals(Direction.East)) {
48+
return Direction.West;
49+
} else if (this.equals(Direction.West)) {
50+
return Direction.East;
51+
} else if (this.equals(Direction.Still)) {
52+
return Direction.Still;
53+
}
54+
throw new Error(`Non-cardinal direction cannot be inverted: ${this}`);
55+
}
56+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Entity } from "./entity";
2+
3+
/** Represents a dropoff. */
4+
export class Dropoff extends Entity {
5+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Dropoff } from "./Dropoff";
2+
import { GameMap } from "./GameMap";
3+
import { Logging } from "./Logging";
4+
import { Player } from "./Player";
5+
import { Position } from "./Position";
6+
import { ServerCommunication } from "./ServerCommunicaion";
7+
import { Ship } from "./Ship";
8+
import { Shipyard } from "./Shipyard";
9+
10+
export class Game {
11+
public turnNumber = 0;
12+
public server: ServerCommunication = new ServerCommunication();
13+
14+
public myId = 0;
15+
public players = new Map<number, Player>();
16+
public me?: Player;
17+
public gameMap?: GameMap;
18+
19+
/**
20+
* Initialize a game object collecting all the start-state
21+
* instances for pre-game. Also sets up a log file in
22+
* "bot-<bot_id>.log".
23+
* @returns The initialized gameMap and me so we don't have to check if undefined.
24+
*/
25+
public async initialize(): Promise<[GameMap, Player]> {
26+
const serverData = await this.server.getInitialData();
27+
this.myId = serverData.myId;
28+
29+
Logging.setup(`bot-${this.myId}.log`);
30+
31+
serverData.players.forEach((playerData) => {
32+
const player = new Player(playerData.id,
33+
new Shipyard(playerData.id, -1, new Position(playerData.x, playerData.y)));
34+
this.players.set(player.id, player);
35+
});
36+
this.me = this.players.get(this.myId);
37+
38+
this.gameMap = new GameMap(serverData.cells);
39+
40+
return [this.gameMap as GameMap, this.me as Player]; // We cast here because we have just initialized
41+
}
42+
43+
/**
44+
* Updates the game object's state.
45+
*/
46+
public async updateFrame() {
47+
const data = await this.server.getUpdateData(this.players.size);
48+
this.turnNumber = data.turn;
49+
Logging.info(`================ TURN ${this.turnNumber.toString().padStart(3, "0")} ================`);
50+
data.players.forEach((playerData) => {
51+
const player = this.players.get(playerData.id) as Player;
52+
player.haliteAmount = playerData.halite;
53+
54+
// Process ships
55+
const newShipsData = playerData.ships
56+
.filter((shipData) => !player.hasShip(shipData.id));
57+
newShipsData.forEach((newShipData) =>
58+
player.addShip(new Ship(player.id, newShipData.id,
59+
new Position(newShipData.x, newShipData.y), newShipData.halite)));
60+
61+
const lostShips = player.getShips()
62+
.filter((ship) => !playerData.ships.some((shipData) => shipData.id === ship.id));
63+
lostShips.forEach((ship) => player.loseShip(ship.id));
64+
65+
player.getShips().forEach((ship) => {
66+
const updatedShipData = playerData.ships
67+
.find((shipData) => ship.id === shipData.id);
68+
if (updatedShipData) {
69+
[ship.haliteAmount, ship.position.x, ship.position.y] =
70+
[updatedShipData.halite, updatedShipData.x, updatedShipData.y];
71+
}
72+
});
73+
74+
// Process dropoffs
75+
const newDropoffsData = playerData.dropoffs
76+
.filter((dropoffData) => !player.dropoffs.has(dropoffData.id));
77+
newDropoffsData.forEach((newDropoffData: { id: number, x: number, y: number }) =>
78+
player.dropoffs.set(newDropoffData.id,
79+
new Dropoff(player.id, newDropoffData.id, new Position(newDropoffData.x, newDropoffData.y))));
80+
81+
const lostDropoffs = Array.from(player.dropoffs.values())
82+
.filter((dropoff) => !playerData.dropoffs.some((dropoffData) => dropoffData.id === dropoff.id));
83+
lostDropoffs.forEach((lostDropoff) => {
84+
player.dropoffs.delete(lostDropoff.id);
85+
player.lostDropoffs.set(lostDropoff.id, lostDropoff);
86+
});
87+
});
88+
89+
const gameMap = this.gameMap as GameMap;
90+
// Mark all cells as safe
91+
gameMap.cells.forEach((row) => row.forEach((cell) => cell.markSafe()));
92+
// Update cells
93+
data.cells.forEach((cell) => gameMap.get(new Position(cell.x, cell.y)).haliteAmount = cell.halite);
94+
// Mark cells with ships as unsafe for navigation, mark sturctures
95+
for (const player of this.players.values()) {
96+
player.getShips()
97+
.forEach((ship) => gameMap.get(ship.position).markUnsafe(ship));
98+
player.getDropoffs()
99+
.forEach((dropoff) => gameMap.get(dropoff.position).structure = dropoff);
100+
}
101+
}
102+
103+
/**
104+
* Indicate that your bot is ready to play by sending the bot name.
105+
*/
106+
public async ready(botName: string) {
107+
await this.server.sendCommands([botName]);
108+
}
109+
110+
/**
111+
* Send all commands to the game engine, effectively ending your
112+
* turn.
113+
*/
114+
public async endTurn(commands: string[]) {
115+
await this.server.sendCommands(commands);
116+
}
117+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Dropoff } from "./Dropoff";
2+
import { Position } from "./Position";
3+
import { Ship } from "./Ship";
4+
import { Shipyard } from "./Shipyard";
5+
6+
/** A cell on the game map. */
7+
export class MapCell {
8+
public ship?: Ship;
9+
public structure?: Shipyard | Dropoff;
10+
11+
constructor(public position: Position, public haliteAmount: number) {
12+
}
13+
14+
/**
15+
* Whether this cell has no ships or structures.
16+
*/
17+
get isEmpty() {
18+
return !this.isOccupied && !this.hasStructure;
19+
}
20+
21+
/**
22+
* Whether this cell has any ships.
23+
*/
24+
get isOccupied() {
25+
return this.ship !== undefined;
26+
}
27+
28+
/**
29+
* Whether this cell has any structures.
30+
*/
31+
get hasStructure() {
32+
return this.structure !== undefined;
33+
}
34+
35+
/**
36+
* Mark this cell as unsafe (occupied) for navigation.
37+
*/
38+
public markUnsafe(ship: Ship) {
39+
this.ship = ship;
40+
}
41+
42+
public markSafe() {
43+
this.ship = undefined;
44+
}
45+
46+
public equals(other: MapCell) {
47+
return this.position.equals(other.position);
48+
}
49+
50+
public toString() {
51+
return `MapCell(${this.position}, halite=${this.haliteAmount})`;
52+
}
53+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Dropoff } from "./Dropoff";
2+
import { Ship } from "./Ship";
3+
import { Shipyard } from "./Shipyard";
4+
5+
/** Player object, containing all entities/metadata for the player. */
6+
export class Player {
7+
public ships = new Map<number, Ship>();
8+
public lostShips = new Map<number, Ship>();
9+
public dropoffs = new Map<number, Dropoff>();
10+
public lostDropoffs = new Map<number, Dropoff>();
11+
constructor(public id: number, public shipyard: Shipyard, public haliteAmount = 0) {
12+
}
13+
14+
/** Get a single ship by its ID. */
15+
public getShip(shipId: number) {
16+
return this.ships.get(shipId);
17+
}
18+
19+
/** Get a list of the player's ships. */
20+
public getShips() {
21+
return Array.from(this.ships.values());
22+
}
23+
24+
/** Get a single dropoff by its ID. */
25+
public getDropoff(dropoffId: number) {
26+
return this.dropoffs.get(dropoffId);
27+
}
28+
29+
/** Get a list of the player's dropoffs. */
30+
public getDropoffs() {
31+
return Array.from(this.dropoffs.values());
32+
}
33+
34+
/** Check whether a ship with a given ID exists. */
35+
public hasShip(shipId: number) {
36+
return this.ships.has(shipId);
37+
}
38+
39+
/**
40+
* Remove ship from the ships list and add it to the lostShips list
41+
* to keep the ship state even when lost.
42+
*/
43+
public loseShip(shipId: number) {
44+
this.lostShips.set(shipId, this.ships.get(shipId) as Ship);
45+
this.ships.delete(shipId);
46+
}
47+
48+
public addShip(ship: Ship) {
49+
this.ships.set(ship.id, ship);
50+
}
51+
}

0 commit comments

Comments
 (0)