Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/test-typescript-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Test TypeScript Starter Kit

# Runs on GitHub-hosted runners (ubuntu-latest).
#
# The TypeScript kit's wire-protocol conformance is enforced at compile time by the typed
# @coderone/bomberland-library package, so this workflow type-checks the kit — both the basic
# agent and the forward-model example (Agent_fwd.ts / ForwardModel.ts) — with tsc.

on:
push:
branches:
- master
pull_request:
branches:
- master
paths:
- "agents/typescript/**"
- ".github/workflows/test-typescript-agent.yaml"
workflow_dispatch:

concurrency:
group: test-typescript-agent-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: agents/typescript
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
cache-dependency-path: agents/typescript/yarn.lock

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Type-check (tsc build)
run: yarn build
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ docker-compose up --abort-on-container-exit --force-recreate
| Python3-fwd | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/python3) | Includes example for using forward model simulator | ✅ | Coder One |
| Python3-gym-wrapper | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/python3) | Open AI Gym wrapper | ✅ | Coder One |
| TypeScript | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/typescript) | Basic TypeScript starter | ✅ | Coder One |
| TypeScript-fwd | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/typescript) | Includes example for using forward model simulator | | Coder One |
| TypeScript-fwd | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/typescript) | Includes example for using forward model simulator | | Coder One |
| Go | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/go) | Basic Go starter | ✅ | [dtitov](https://github.com/dtitov) |
| C++ | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/cpp) | Basic C++ starter | ✅ | [jfbogusz](https://github.com/jfbogusz) |
| Rust | [Link](https://github.com/CoderOneHQ/bomberland/tree/master/agents/rust) | Basic Rust starter | ✅ | [K-JBoon](https://github.com/K-JBoon) |
Expand Down
3 changes: 3 additions & 0 deletions agents/typescript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
node_modules/
*.tsbuildinfo
2 changes: 1 addition & 1 deletion agents/typescript/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:12.19.0-alpine3.9
FROM node:22-alpine
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock

Expand Down
2 changes: 1 addition & 1 deletion agents/typescript/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:12.19.0-alpine3.9
FROM node:22-alpine
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock

Expand Down
9 changes: 9 additions & 0 deletions agents/typescript/Dockerfile.fwd
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:22-alpine
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock

WORKDIR /app
RUN yarn install
COPY . /app/
RUN yarn build
ENTRYPOINT yarn run run:fwd
7 changes: 7 additions & 0 deletions agents/typescript/Dockerfile.fwd.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM node:22-alpine
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock

WORKDIR /app
RUN yarn install
ENTRYPOINT yarn install && yarn run start:fwd
8 changes: 6 additions & 2 deletions agents/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
"private": true,
"dependencies": {
"@coderone/bomberland-library": "^4.0.0",
"typescript": "^4.7.3"
"typescript": "^4.7.3",
"ws": "^8.8.1"
},
"scripts": {
"check-dependencies": "depcheck",
"build": "tsc --build tsconfig.json",
"start": "tsc --build tsconfig.json && concurrently \"tsc --watch\" \"nodemon ./dist/Agent.js\"",
"run": "node ./dist/Agent.js"
"start:fwd": "tsc --build tsconfig.json && concurrently \"tsc --watch\" \"nodemon ./dist/Agent_fwd.js\"",
"run": "node ./dist/Agent.js",
"run:fwd": "node ./dist/Agent_fwd.js"
},
"devDependencies": {
"@types/node": "^14.14.41",
"@types/ws": "^8.5.3",
"concurrently": "^5.3.0",
"depcheck": "^1.4.0",
"nodemon": "^2.0.7"
Expand Down
107 changes: 107 additions & 0 deletions agents/typescript/src/Agent_fwd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { AgentPacket, EntityType, GameStateClient, IGameState, PacketType, UnitMove } from "@coderone/bomberland-library";
import { ForwardAction, ForwardModel, NextStatePayload } from "./ForwardModel";

const gameConnectionString =
process.env["GAME_CONNECTION_STRING"] || "ws://127.0.0.1:3000/?role=agent&agentId=agentIdA&name=RandomAgentFwd";
const fwdModelConnectionString = process.env["FWD_MODEL_CONNECTION_STRING"] || "ws://127.0.0.1:6969/?role=admin";

enum Action {
Up = "up",
Down = "down",
Left = "left",
Right = "right",
Bomb = "bomb",
Detonate = "detonate",
}

const actionMoveMap = new Map<Action, UnitMove>([
[Action.Up, UnitMove.Up],
[Action.Down, UnitMove.Down],
[Action.Left, UnitMove.Left],
[Action.Right, UnitMove.Right],
]);

const actionList = Object.values(Action);

class Agent {
private readonly client = new GameStateClient(gameConnectionString);
private readonly forwardModel = new ForwardModel(fwdModelConnectionString);
private sequenceId = 0;

public constructor() {
this.client.SetGameTickCallback(this.onGameTick);
this.forwardModel.SetNextStateCallback(this.onNextGameState);
}

private onGameTick = async (gameState: Omit<IGameState, "connection"> | undefined) => {
if (gameState === undefined) {
return;
}

// Forward-model demo: ask "what happens next tick if all my units move right?"
// before committing to a real action. A real agent would evaluate several candidate
// action sets and pick the best-scoring resulting state.
this.evaluateNextState(gameState);

const myAgentId = this.client.Connection?.agent_id ?? "";
const units = gameState.agents[myAgentId]?.unit_ids ?? [];
units.forEach((unitId) => {
const action = this.generateAction();
if (action) {
const mappedMove = actionMoveMap.get(action);
if (mappedMove !== undefined) {
this.client.SendMove(unitId, mappedMove);
} else if (action === Action.Bomb) {
this.client.SendPlaceBomb(unitId);
} else if (action === Action.Detonate) {
const bombCoordinates = this.getBombToDetonate(gameState);
if (bombCoordinates !== undefined) {
this.client.SendDetonateBomb(unitId, bombCoordinates);
}
}
}
});
};

private evaluateNextState = (gameState: Omit<IGameState, "connection">) => {
const myAgentId = this.client.Connection?.agent_id ?? "";
const units = gameState.agents[myAgentId]?.unit_ids ?? [];
const actions: Array<ForwardAction> = units.map((unitId) => {
const action: AgentPacket = { type: PacketType.Move, move: UnitMove.Right, unit_id: unitId };
return { agent_id: myAgentId, action };
});
this.forwardModel.SendEvaluateNextState(this.sequenceId++, gameState, actions);
};

private onNextGameState = (payload: NextStatePayload) => {
// The forward model has simulated the hypothetical actions. `payload.next_state` is the
// resulting state you would score in a search; here we just log a summary.
console.log(
`Forward model seq=${payload.sequence_id}: is_complete=${payload.is_complete}, ` +
`resulting events=${payload.tick_result.events.length}, next tick=${payload.next_state.tick}`
);
};

private generateAction = (): Action | undefined => {
const allActions = actionList.length;
const rand = Math.round(Math.random() * allActions);
if (rand !== allActions) {
return actionList[rand];
}
};

private getBombToDetonate = (gameState: Omit<IGameState, "connection">): [number, number] | undefined => {
const currentAgent = this.client.Connection?.agent_id;
const bomb = gameState.entities.find((entity) => {
const isBomb = entity.type === EntityType.Bomb;
const isOwner = currentAgent !== undefined ? entity.agent_id === currentAgent : false;
return isBomb === true && isOwner === true;
});

if (bomb?.x !== undefined && bomb.y !== undefined) {
return [bomb.x, bomb.y];
}
};
}

new Agent();
77 changes: 77 additions & 0 deletions agents/typescript/src/ForwardModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import WebSocket from "ws";
import { AgentPacket, EvaluateNextStatePacket, IGameState, NextGameStatePacket, PacketType } from "@coderone/bomberland-library";

export type ForwardAction = {
readonly agent_id: string;
readonly action: AgentPacket;
};

export type NextStatePayload = NextGameStatePacket["payload"];
export type NextStateCallback = (payload: NextStatePayload) => void;

/**
* Client for the forward-model simulator. The forward model is a separate engine instance run
* with `role=admin` (see the `fwd-server` service in open-ai-gym-wrapper-compose.yml, default
* port 6969). You send it a state plus a set of hypothetical actions and it returns the
* resulting next state — useful for lookahead / tree search without mutating the live game.
*
* The bundled `@coderone/bomberland-library` GameStateClient can send EvaluateNextState but does
* not surface the `next_game_state` reply, so this small client owns its own socket instead.
*/
export class ForwardModel {
private readonly socket: WebSocket;
private onNextState: NextStateCallback | undefined;

public constructor(connectionString: string) {
this.socket = new WebSocket(connectionString);
this.attachHandlers();
}

public SetNextStateCallback = (callback: NextStateCallback | undefined): void => {
this.onNextState = callback;
};

/**
* Ask the forward model to evaluate `actions` applied to `state`.
* `sequenceId` correlates the response, since replies may arrive out of order.
*/
public SendEvaluateNextState = (
sequenceId: number,
state: Omit<IGameState, "connection">,
actions: ReadonlyArray<ForwardAction>
): void => {
const packet: EvaluateNextStatePacket = {
type: PacketType.EvaluateNextState,
sequence_id: sequenceId,
state,
actions: [...actions],
};
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(packet));
}
};

public Destroy = (): void => {
this.socket.close();
};

private attachHandlers = (): void => {
this.socket.on("open", () => {
console.log("Forward model connection opened");
});
this.socket.on("message", (data: WebSocket.RawData) => {
try {
const packet = JSON.parse(data.toString());
if (packet.type === PacketType.NextGameState) {
this.onNextState?.((packet as NextGameStatePacket).payload);
}
// Other packets on the admin socket (e.g. info) are not needed here.
} catch (error) {
console.error(`Failed to parse forward-model message: ${error}`);
}
});
this.socket.on("error", (error) => {
console.error(`Forward model socket error: ${error}`);
});
};
}
1 change: 1 addition & 0 deletions agents/typescript/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lib": ["es6", "dom"],
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strict": true
Expand Down
30 changes: 27 additions & 3 deletions agents/typescript/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,30 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==

"@types/node@*":
version "26.1.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.1.tgz#bad758d601e97d6cf457d204ee76a35fce7bd119"
integrity sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==
dependencies:
undici-types "~8.3.0"

"@types/node@^14.14.41":
version "14.14.41"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615"
integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g==
version "14.18.63"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b"
integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==

"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==

"@types/ws@^8.5.3":
version "8.18.1"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9"
integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==
dependencies:
"@types/node" "*"

"@vue/compiler-core@3.0.11":
version "3.0.11"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.11.tgz#5ef579e46d7b336b8735228758d1c2c505aae69a"
Expand Down Expand Up @@ -1726,6 +1740,11 @@ undefsafe@^2.0.3:
dependencies:
debug "^2.2.0"

undici-types@~8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809"
integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==

unique-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
Expand Down Expand Up @@ -1836,6 +1855,11 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"

ws@^8.8.1:
version "8.21.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.1.tgz#045650cd4b1207809e7547146223c3814a9af586"
integrity sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==

xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
Expand Down
12 changes: 12 additions & 0 deletions base-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ services:
volumes:
- ./agents/typescript:/app

typescript-fwd:
build:
context: agents/typescript
dockerfile: Dockerfile.fwd

typescript-fwd-dev:
build:
context: agents/typescript
dockerfile: Dockerfile.fwd.dev
volumes:
- ./agents/typescript:/app

cpp-agent:
build:
context: agents/cpp
Expand Down
Loading