From 27a37ecb21fdef1e7cb2d3a4e51ab71fa4250f41 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 25 Jul 2026 10:31:32 -0700 Subject: [PATCH 1/3] Add TypeScript forward-model example The README listed a "TypeScript-fwd" kit but no forward-model example ever existed (only the basic Agent.ts). This adds one, mirroring the Python-fwd setup. The forward model is a separate engine instance run with role=admin (default port 6969): you send it a state + hypothetical actions and it returns the resulting next state, for lookahead / tree search. The bundled @coderone/bomberland-library GameStateClient can send EvaluateNextState but does not surface the next_game_state reply (its message handler has no next-state case), so: - ForwardModel.ts: a small client owning its own `ws` socket that sends typed EvaluateNextState packets and delivers next_game_state via a callback. - Agent_fwd.ts: the basic random agent plus a per-tick forward-model call that logs the predicted next state. - package.json: add `ws` + `@types/ws`, and run:fwd / start:fwd scripts. - tsconfig: skipLibCheck (the pinned typescript@4.7 chokes on newer @types/node and @types/ws .d.ts files; our own code is still fully type-checked). - Dockerfile.fwd / Dockerfile.fwd.dev and base-compose typescript-fwd(-dev) services, mirroring python3-fwd. - Bump all four TS Dockerfiles off EOL node:12 to node:22-alpine. - Add a scoped .gitignore (the root ignore's `typescript/dist` never matched agents/typescript/dist, so build output would have been committed). Verified end-to-end against a running engine: the client captures a real game_state, sends evaluate_next_state, and receives the matching next_game_state back through the callback. --- agents/typescript/.gitignore | 3 + agents/typescript/Dockerfile | 2 +- agents/typescript/Dockerfile.dev | 2 +- agents/typescript/Dockerfile.fwd | 9 +++ agents/typescript/Dockerfile.fwd.dev | 7 ++ agents/typescript/package.json | 8 +- agents/typescript/src/Agent_fwd.ts | 107 ++++++++++++++++++++++++++ agents/typescript/src/ForwardModel.ts | 77 ++++++++++++++++++ agents/typescript/tsconfig.json | 1 + agents/typescript/yarn.lock | 30 +++++++- base-compose.yml | 12 +++ 11 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 agents/typescript/.gitignore create mode 100644 agents/typescript/Dockerfile.fwd create mode 100644 agents/typescript/Dockerfile.fwd.dev create mode 100644 agents/typescript/src/Agent_fwd.ts create mode 100644 agents/typescript/src/ForwardModel.ts diff --git a/agents/typescript/.gitignore b/agents/typescript/.gitignore new file mode 100644 index 00000000..83631f81 --- /dev/null +++ b/agents/typescript/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +*.tsbuildinfo diff --git a/agents/typescript/Dockerfile b/agents/typescript/Dockerfile index d896abdc..6db93a4e 100644 --- a/agents/typescript/Dockerfile +++ b/agents/typescript/Dockerfile @@ -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 diff --git a/agents/typescript/Dockerfile.dev b/agents/typescript/Dockerfile.dev index c99bd7ac..bbe3dcae 100644 --- a/agents/typescript/Dockerfile.dev +++ b/agents/typescript/Dockerfile.dev @@ -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 diff --git a/agents/typescript/Dockerfile.fwd b/agents/typescript/Dockerfile.fwd new file mode 100644 index 00000000..da23971d --- /dev/null +++ b/agents/typescript/Dockerfile.fwd @@ -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 diff --git a/agents/typescript/Dockerfile.fwd.dev b/agents/typescript/Dockerfile.fwd.dev new file mode 100644 index 00000000..9c9e68f1 --- /dev/null +++ b/agents/typescript/Dockerfile.fwd.dev @@ -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 diff --git a/agents/typescript/package.json b/agents/typescript/package.json index 47bd4ed4..2e809bbb 100644 --- a/agents/typescript/package.json +++ b/agents/typescript/package.json @@ -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" diff --git a/agents/typescript/src/Agent_fwd.ts b/agents/typescript/src/Agent_fwd.ts new file mode 100644 index 00000000..4d7217c7 --- /dev/null +++ b/agents/typescript/src/Agent_fwd.ts @@ -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.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 | 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) => { + const myAgentId = this.client.Connection?.agent_id ?? ""; + const units = gameState.agents[myAgentId]?.unit_ids ?? []; + const actions: Array = 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): [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(); diff --git a/agents/typescript/src/ForwardModel.ts b/agents/typescript/src/ForwardModel.ts new file mode 100644 index 00000000..4b8e20cd --- /dev/null +++ b/agents/typescript/src/ForwardModel.ts @@ -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, + actions: ReadonlyArray + ): 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}`); + }); + }; +} diff --git a/agents/typescript/tsconfig.json b/agents/typescript/tsconfig.json index 235f7873..6c793099 100644 --- a/agents/typescript/tsconfig.json +++ b/agents/typescript/tsconfig.json @@ -9,6 +9,7 @@ "lib": ["es6", "dom"], "esModuleInterop": true, "moduleResolution": "node", + "skipLibCheck": true, "strictNullChecks": true, "noImplicitAny": true, "strict": true diff --git a/agents/typescript/yarn.lock b/agents/typescript/yarn.lock index 677091a4..4e2f2727 100644 --- a/agents/typescript/yarn.lock +++ b/agents/typescript/yarn.lock @@ -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" @@ -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" @@ -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" diff --git a/base-compose.yml b/base-compose.yml index 93bcffa6..842c897d 100644 --- a/base-compose.yml +++ b/base-compose.yml @@ -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 From e2a490181fe39e7c4bfcd9d5cd0d838ebd717ee4 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 25 Jul 2026 10:31:40 -0700 Subject: [PATCH 2/3] Add CI for TypeScript starter kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type-checks the kit (basic agent + forward-model example) with tsc. The TypeScript kit's wire-protocol conformance is enforced at compile time by the typed @coderone/bomberland-library, so a tsc build is the meaningful check (no engine-schema drift gate as with Go/Rust — the types come from the npm package, not the schema file). - Triggers on changes to agents/typescript/** and the workflow. - yarn install --frozen-lockfile + yarn build on node 22. GitHub-hosted runners. --- .github/workflows/test-typescript-agent.yaml | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/test-typescript-agent.yaml diff --git a/.github/workflows/test-typescript-agent.yaml b/.github/workflows/test-typescript-agent.yaml new file mode 100644 index 00000000..54938c25 --- /dev/null +++ b/.github/workflows/test-typescript-agent.yaml @@ -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 From 8caa109c68f5c41ea876b0f41d4b43d3e88851bc Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 25 Jul 2026 10:31:40 -0700 Subject: [PATCH 3/3] docs: mark TypeScript-fwd as up-to-date Adds the forward-model example (Agent_fwd.ts / ForwardModel.ts) that the row promised, type-checked in CI (test-typescript-agent.yaml). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b3d0a6f..2053d275 100644 --- a/README.md +++ b/README.md @@ -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) |