From 714279d25090c57d28468aceb1d9343843cf0b64 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sat, 21 Feb 2026 17:15:40 +0100 Subject: [PATCH 01/15] add TypeScript bot templates and devcontainer setup for "softcore" and "hardcore" bots --- .github/workflows/game.yml | 137 +++++++++ .github/workflows/my-core-bot-ts-Dockerfile | 18 ++ bots/ts/client_lib/client_lib.ts | 269 ++++++++++++++++++ bots/ts/client_lib/package.json | 10 + bots/ts/client_lib/types.ts | 122 ++++++++ bots/ts/hardcore/.coreignore | 0 .../hardcore/.devcontainer/devcontainer.json | 56 ++++ .../hardcore/.devcontainer/docker-compose.yml | 18 ++ .../hardcore/.devcontainer/init_docker.bash | 77 +++++ bots/ts/hardcore/.gitignore | 9 + bots/ts/hardcore/Makefile | 204 +++++++++++++ bots/ts/hardcore/README.md | 64 +++++ bots/ts/hardcore/gridmaster/index.ts | 33 +++ bots/ts/hardcore/gridmaster/package.json | 12 + bots/ts/hardcore/my-core-bot/index.ts | 32 +++ bots/ts/hardcore/my-core-bot/package.json | 12 + bots/ts/hardcore/package.json | 14 + .../replays/file to keep folder in git | 0 .../hardcore/scripts/check_image_updates.sh | 131 +++++++++ .../hardcore/scripts/check_update_configs.sh | 38 +++ bots/ts/softcore/.coreignore | 0 .../softcore/.devcontainer/devcontainer.json | 56 ++++ .../softcore/.devcontainer/docker-compose.yml | 18 ++ .../softcore/.devcontainer/init_docker.bash | 77 +++++ bots/ts/softcore/.gitignore | 9 + bots/ts/softcore/Makefile | 206 ++++++++++++++ bots/ts/softcore/README.md | 64 +++++ bots/ts/softcore/gridmaster/index.ts | 25 ++ bots/ts/softcore/gridmaster/package.json | 12 + bots/ts/softcore/my-core-bot/index.ts | 24 ++ bots/ts/softcore/my-core-bot/package.json | 12 + bots/ts/softcore/package.json | 14 + .../replays/file to keep folder in git | 0 .../softcore/scripts/check_image_updates.sh | 131 +++++++++ .../softcore/scripts/check_update_configs.sh | 38 +++ 35 files changed, 1942 insertions(+) create mode 100644 .github/workflows/my-core-bot-ts-Dockerfile create mode 100644 bots/ts/client_lib/client_lib.ts create mode 100644 bots/ts/client_lib/package.json create mode 100644 bots/ts/client_lib/types.ts create mode 100644 bots/ts/hardcore/.coreignore create mode 100644 bots/ts/hardcore/.devcontainer/devcontainer.json create mode 100644 bots/ts/hardcore/.devcontainer/docker-compose.yml create mode 100644 bots/ts/hardcore/.devcontainer/init_docker.bash create mode 100644 bots/ts/hardcore/.gitignore create mode 100644 bots/ts/hardcore/Makefile create mode 100644 bots/ts/hardcore/README.md create mode 100644 bots/ts/hardcore/gridmaster/index.ts create mode 100644 bots/ts/hardcore/gridmaster/package.json create mode 100644 bots/ts/hardcore/my-core-bot/index.ts create mode 100644 bots/ts/hardcore/my-core-bot/package.json create mode 100644 bots/ts/hardcore/package.json create mode 100644 bots/ts/hardcore/replays/file to keep folder in git create mode 100755 bots/ts/hardcore/scripts/check_image_updates.sh create mode 100644 bots/ts/hardcore/scripts/check_update_configs.sh create mode 100644 bots/ts/softcore/.coreignore create mode 100644 bots/ts/softcore/.devcontainer/devcontainer.json create mode 100644 bots/ts/softcore/.devcontainer/docker-compose.yml create mode 100644 bots/ts/softcore/.devcontainer/init_docker.bash create mode 100644 bots/ts/softcore/.gitignore create mode 100644 bots/ts/softcore/Makefile create mode 100644 bots/ts/softcore/README.md create mode 100644 bots/ts/softcore/gridmaster/index.ts create mode 100644 bots/ts/softcore/gridmaster/package.json create mode 100644 bots/ts/softcore/my-core-bot/index.ts create mode 100644 bots/ts/softcore/my-core-bot/package.json create mode 100644 bots/ts/softcore/package.json create mode 100644 bots/ts/softcore/replays/file to keep folder in git create mode 100755 bots/ts/softcore/scripts/check_image_updates.sh create mode 100755 bots/ts/softcore/scripts/check_update_configs.sh diff --git a/.github/workflows/game.yml b/.github/workflows/game.yml index ebfdee20..ae04133c 100644 --- a/.github/workflows/game.yml +++ b/.github/workflows/game.yml @@ -25,6 +25,7 @@ permissions: env: SERVER_IMAGE: ghcr.io/${{ github.repository_owner }}/server BOT_IMAGE: ghcr.io/${{ github.repository_owner }}/my-core-bot-c + BOT_TS_IMAGE: ghcr.io/${{ github.repository_owner }}/my-core-bot-ts RUNNER_AMD64: &RUNNER_AMD64 blacksmith-4vcpu-ubuntu-2404 # set this to ubuntu-24.04 for github hosted runners and X64 for self-hosted runners RUNNER_ARM64: &RUNNER_ARM64 blacksmith-4vcpu-ubuntu-2404-arm # set this to ubuntu-24.04-arm for github hosted runners and ARM64 for self-hosted runners @@ -85,6 +86,20 @@ jobs: push: false provenance: false + - name: Build my-core-bot-ts (PR, local only) + uses: docker/build-push-action@v6 + with: + context: ${{ github.workspace }} + file: .github/workflows/my-core-bot-ts-Dockerfile + builder: ${{ steps.builder.outputs.name }} + platforms: linux/amd64 + build-args: | + SERVER_IMAGE=local/server + TAG_NAME=pr-${{ github.event.pull_request.head.sha }} + pull: false + push: false + provenance: false + # Per-arch build: build server, tag temp per-arch for bot, then build bot โ€” all in one job/matrix build-per-arch: if: github.event_name != 'pull_request' @@ -128,6 +143,12 @@ jobs: with: images: ${{ env.BOT_IMAGE }} + - name: Docker meta (my-core-bot-ts) + id: meta-bot-ts + uses: docker/metadata-action@v5 + with: + images: ${{ env.BOT_TS_IMAGE }} + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: @@ -197,6 +218,36 @@ jobs: if-no-files-found: error retention-days: 1 + # Build TS bot on same runner arch, consuming the temp-tagged server image + - name: Build and push by digest (my-core-bot-ts) + id: build-bot-ts + uses: useblacksmith/build-push-action@v2 + with: + context: ${{ github.workspace }} + file: .github/workflows/my-core-bot-ts-Dockerfile + build-args: | + SERVER_IMAGE=${{ env.SERVER_IMAGE }} + TAG_NAME=${{ env.TEMP_TAG }} + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta-bot-ts.outputs.labels }} + tags: ${{ env.BOT_TS_IMAGE }} + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + provenance: false + + - name: Export digest (my-core-bot-ts) + run: | + mkdir -p ${{ runner.temp }}/digests-bot-ts + digest="${{ steps.build-bot-ts.outputs.digest }}" + touch "${{ runner.temp }}/digests-bot-ts/${digest#sha256:}" + + - name: Upload digest (my-core-bot-ts) + uses: actions/upload-artifact@v4 + with: + name: bot-ts-digests-${{ matrix.arch }} + path: ${{ runner.temp }}/digests-bot-ts/* + if-no-files-found: error + retention-days: 1 + # Merge server digests into multi-arch manifest push-server-merge: if: github.event_name != 'pull_request' @@ -369,3 +420,89 @@ jobs: - name: Inspect image (my-core-bot-c) run: | docker buildx imagetools inspect ${{ env.BOT_IMAGE }}:${{ steps.meta-bot.outputs.version }} + + # Merge bot digests into multi-arch manifest (TS) + push-bot-ts-merge: + if: github.event_name != 'pull_request' + runs-on: *RUNNER_AMD64 + needs: build-per-arch + steps: + - name: Download digests (my-core-bot-ts) + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests-bot-ts + pattern: bot-ts-digests-* + merge-multiple: true + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Blacksmith Builder + uses: useblacksmith/setup-docker-builder@v1 + + - name: Docker meta (my-core-bot-ts) + id: meta-bot-ts + uses: docker/metadata-action@v5 + with: + images: ${{ env.BOT_TS_IMAGE }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=raw,value=${{ github.ref_name }}-${{ github.sha }} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} + + - name: Generate additional version tags (my-core-bot-ts) + id: version-tags-bot-ts + if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-') + run: | + # Extract version from tag (remove 'v' prefix if present) + VERSION=${GITHUB_REF#refs/tags/} + VERSION=${VERSION#v} + + # Split version into parts + IFS='.' read -r -a parts <<< "$VERSION" + + # Generate hierarchical tags based on number of parts + ADDITIONAL_TAGS="" + if [ ${#parts[@]} -ge 1 ]; then + ADDITIONAL_TAGS="${{ env.BOT_TS_IMAGE }}:v${parts[0]}" + fi + if [ ${#parts[@]} -ge 2 ]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,${{ env.BOT_TS_IMAGE }}:v${parts[0]}.${parts[1]}" + fi + if [ ${#parts[@]} -ge 3 ]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,${{ env.BOT_TS_IMAGE }}:v${parts[0]}.${parts[1]}.${parts[2]}" + fi + if [ ${#parts[@]} -ge 4 ]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,${{ env.BOT_TS_IMAGE }}:v${parts[0]}.${parts[1]}.${parts[2]}.${parts[3]}" + fi + + echo "additional-tags=$ADDITIONAL_TAGS" >> $GITHUB_OUTPUT + echo "Generated additional tags: $ADDITIONAL_TAGS" + + - name: Create manifest list and push (my-core-bot-ts) + working-directory: ${{ runner.temp }}/digests-bot-ts + run: | + # Create manifest for base tags from metadata + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.BOT_TS_IMAGE }}@sha256:%s ' *) + + # Create manifest for additional version tags if they exist + if [ -n "${{ steps.version-tags-bot-ts.outputs.additional-tags }}" ]; then + IFS=',' read -r -a additional_tags <<< "${{ steps.version-tags-bot-ts.outputs.additional-tags }}" + for tag in "${additional_tags[@]}"; do + if [ -n "$tag" ]; then + echo "Creating manifest for additional tag: $tag" + docker buildx imagetools create -t "$tag" \ + $(printf '${{ env.BOT_TS_IMAGE }}@sha256:%s ' *) + fi + done + fi + + - name: Inspect image (my-core-bot-ts) + run: | + docker buildx imagetools inspect ${{ env.BOT_TS_IMAGE }}:${{ steps.meta-bot-ts.outputs.version }} diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile new file mode 100644 index 00000000..d46d9c3a --- /dev/null +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -0,0 +1,18 @@ +ARG TAG_NAME="dev" +ARG SERVER_IMAGE="ghcr.io/42core-team/server" + +FROM oven/bun:latest AS bun-base +WORKDIR /app + +FROM ${SERVER_IMAGE}:${TAG_NAME} AS game + +# Build connection library from monorepo sources +FROM bun-base AS connection +WORKDIR /connection +COPY bots/ts/client_lib/ ./ + +FROM bun-base AS release + +COPY --from=game /core/server /core/server +COPY --from=game /core/data /core/data +COPY --from=connection /connection/ /core/ts/client_lib/ diff --git a/bots/ts/client_lib/client_lib.ts b/bots/ts/client_lib/client_lib.ts new file mode 100644 index 00000000..ad6002c7 --- /dev/null +++ b/bots/ts/client_lib/client_lib.ts @@ -0,0 +1,269 @@ +import * as net from 'net'; +import type { GameState, Action, Obj, Pos } from './types'; +import { ActionType, UnitType, ObjType, ObjState } from './types'; + +export class ClientLib { + private socket: net.Socket | null = null; + private game: GameState | null = null; + private actions: Action[] = []; + private debugData: any[] = []; + private buffer: string = ''; + + constructor(private teamName: string, private teamId: number, private debug: boolean = false) {} + + public async connect(host: string = '127.0.0.1', port: number = 4444): Promise { + return new Promise((resolve, reject) => { + this.socket = new net.Socket(); + + this.socket.connect(port, host, () => { + if (this.debug) console.log(`Connected to ${host}:${port}`); + this.sendLogin(); + }); + + this.socket.on('data', (data: Buffer) => { + this.handleData(data); + if (this.game && resolve) { + resolve(); + (resolve as any) = null; + } + }); + + this.socket.on('error', (err) => { + console.error('Socket error:', err); + reject(err); + }); + + this.socket.on('close', () => { + if (this.debug) console.log('Connection closed'); + process.exit(0); + }); + }); + } + + private sendLogin(): void { + const loginMsg = { + password: "42", + id: this.teamId, + name: this.teamName + }; + this.sendJson(loginMsg); + } + + private sendJson(obj: any): void { + if (this.socket) { + const str = JSON.stringify(obj) + '\n'; + this.socket.write(str); + if (this.debug) console.log('Sent:', str); + } + } + + private handleData(data: Buffer): void { + this.buffer += data.toString(); + let newlineIndex; + while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) { + const line = this.buffer.slice(0, newlineIndex); + this.buffer = this.buffer.slice(newlineIndex + 1); + if (line.trim()) { + this.handleLine(line); + } + } + } + + private handleLine(line: string): void { + try { + const parsed = JSON.parse(line); + if (this.debug) console.log('Received:', line); + + if (!this.game) { + // Initial config + this.game = { + elapsed_ticks: 0, + config: parsed, + my_team_id: this.teamId, + objects: [] + }; + if (this.debug) console.log('Config received'); + // Send first packet of actions right after receiving config to start game loop + this.sendActions(); + } else { + // Game state update + if (parsed.tick !== undefined) { + this.game.elapsed_ticks = parsed.tick; + } + + if (parsed.objects && Array.isArray(parsed.objects)) { + for (const diff of parsed.objects) { + this.applyDiff(diff); + } + } + + // print errors from last tick if there were any + if (parsed.errors && Array.isArray(parsed.errors)) { + for (const error of parsed.errors) { + console.error(`\x1b[31m${error}\x1b[0m`); + } + } + + // Decrement cooldowns manually as C lib does + for (const obj of this.game.objects) { + if (obj.type === ObjType.UNIT) { + if (obj.s_unit.action_cooldown > 0) obj.s_unit.action_cooldown--; + } else if (obj.type === ObjType.CORE) { + if (obj.s_core.spawn_cooldown > 0) obj.s_core.spawn_cooldown--; + } + } + + // After state update, we should have a tick + if (this.tickCallback) { + this.tickCallback(this.game); + } + // ALWAYS send actions back to the server to prevent timeouts + this.sendActions(); + } + } catch (e) { + console.error('Error parsing JSON:', e, 'Line:', line); + } + } + + private applyDiff(diff: any): void { + if (!this.game) return; + + const id = diff.id; + if (id === undefined) return; + + if (diff.state === 'dead') { + this.game.objects = this.game.objects.filter(o => o.id !== id); + return; + } + + let obj = this.game.objects.find(o => o.id === id); + if (!obj) { + obj = { + id, + state: ObjState.ALIVE, + pos: { x: 0, y: 0 }, + s_core: { team_id: 0, gems: 0, spawn_cooldown: 0, balance: 0 }, + s_unit: { unit_type: 0, team_id: 0, gems: 0, action_cooldown: 0, balance: 0 }, + s_deposit_gems_pile: { gems: 0 }, + s_bomb: { countdown: 0 } + } as Obj; + this.game.objects.push(obj); + } + + if (diff.type !== undefined) obj.type = diff.type; + if (diff.x !== undefined || diff.y !== undefined) { + if (diff.x !== undefined) obj.pos.x = diff.x; + if (diff.y !== undefined) obj.pos.y = diff.y; + } + if (diff.hp !== undefined) obj.hp = diff.hp; + + if (diff.teamId !== undefined) { + obj.s_core.team_id = diff.teamId; + obj.s_unit.team_id = diff.teamId; + } + if (diff.gems !== undefined) { + obj.s_core.gems = diff.gems; + obj.s_core.balance = diff.gems; + obj.s_unit.gems = diff.gems; + obj.s_unit.balance = diff.gems; + obj.s_deposit_gems_pile.gems = diff.gems; + } + if (diff.ActionCooldown !== undefined) obj.s_unit.action_cooldown = diff.ActionCooldown; + if (diff.SpawnCooldown !== undefined) obj.s_core.spawn_cooldown = diff.SpawnCooldown; + if (diff.unit_type !== undefined) obj.s_unit.unit_type = diff.unit_type; + if (diff.countdown !== undefined) obj.s_bomb.countdown = diff.countdown; + } + + private tickCallback: ((game: GameState) => void) | null = null; + + public startGame(callback: (game: GameState) => void): void { + this.tickCallback = callback; + } + + private sendActions(): void { + const packet = { + actions: this.actions, + debug_data: this.debugData + }; + this.sendJson(packet); + this.actions = []; + this.debugData = []; + } + + public createUnit(unitType: UnitType): void { + this.actions.push({ + type: ActionType.CREATE, + unit_type: unitType + }); + } + + public move(unit: Obj, pos: Pos): void { + this.actions.push({ + type: ActionType.MOVE, + unit_id: unit.id, + x: pos.x, + y: pos.y + }); + } + + public attack(attacker: Obj, target: Obj): void { + this.actions.push({ + type: ActionType.ATTACK, + unit_id: attacker.id, + target_id: target.id + }); + } + + public transferGems(source: Obj, targetPos: Pos, amount: number): void { + this.actions.push({ + type: ActionType.TRANSFER, + source_id: source.id, + x: targetPos.x, + y: targetPos.y, + amount: amount + }); + } + + public build(builder: Obj, pos: Pos): void { + this.actions.push({ + type: ActionType.BUILD, + unit_id: builder.id, + x: pos.x, + y: pos.y + }); + } + + public addDebugData(data: any): void { + this.debugData.push(data); + } + + public core_debug_addObjectInfo(obj: Obj, info: string): void { + let entry = this.debugData.find(d => d.object_id === obj.id); + if (!entry) { + entry = { + object_id: obj.id, + object_info: "", + object_path: [] + }; + this.debugData.push(entry); + } + entry.object_info += info; + } + + public core_debug_addObjectPathStep(unit: Obj, pos: Pos): void { + let entry = this.debugData.find(d => d.object_id === unit.id); + if (!entry) { + entry = { + object_id: unit.id, + object_info: "", + object_path: [] + }; + this.debugData.push(entry); + } + entry.object_path.push({ x: pos.x, y: pos.y }); + } + + public getGame(): GameState | null { + return this.game; + } +} diff --git a/bots/ts/client_lib/package.json b/bots/ts/client_lib/package.json new file mode 100644 index 00000000..8de25363 --- /dev/null +++ b/bots/ts/client_lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@core/client-lib", + "version": "1.0.0", + "description": "TypeScript client library for Core game", + "main": "client_lib.ts", + "types": "types.ts", + "dependencies": { + "@types/node": "^20.11.0" + } +} diff --git a/bots/ts/client_lib/types.ts b/bots/ts/client_lib/types.ts new file mode 100644 index 00000000..f686deda --- /dev/null +++ b/bots/ts/client_lib/types.ts @@ -0,0 +1,122 @@ +export interface Pos { + x: number; + y: number; +} + +export enum ObjType { + CORE = 0, + UNIT = 1, + RESOURCE = 2, + WALL = 3, + MONEY = 4, + BOMB = 5 +} + +export enum UnitType { + WARRIOR = 0, + MINER = 1, + CARRIER = 2, + TANK = 3 +} + +export enum BuildType { + NONE = 0, + WALL = 1, + BOMB = 2 +} + +export enum ObjState { + ALIVE = "alive", + DEAD = "dead" +} + +export interface UnitConfig { + name: string; + unit_type: UnitType; + cost: number; + hp: number; + baseActionCooldown: number; + maxActionCooldown: number; + balancePerCooldownStep: number; + dmg_core: number; + dmg_unit: number; + dmg_deposit: number; + dmg_wall: number; + dmg_bomb: number; + build_type: BuildType; +} + +export interface Config { + gridSize: number; + idle_income: number; + idle_income_timeout: number; + deposit_hp: number; + deposit_income: number; + gem_pile_income: number; + core_hp: number; + core_spawn_cooldown: number; + initial_balance: number; + wall_hp: number; + wall_build_cost: number; + bomb_countdown: number; + bomb_throw_cost: number; + bomb_reach: number; + bomb_damage_core: number; + bomb_damage_unit: number; + bomb_damage_deposit: number; + units: UnitConfig[]; +} + +export interface Obj { + type: ObjType; + id: number; + pos: Pos; + hp: number; + state: ObjState; + s_core: { + team_id: number; + gems: number; + spawn_cooldown: number; + balance: number; + }; + s_unit: { + unit_type: UnitType; + team_id: number; + gems: number; + action_cooldown: number; + balance: number; + }; + s_deposit_gems_pile: { + gems: number; + }; + s_bomb: { + countdown: number; + }; +} + +export type ObjUnit = Obj & { type: ObjType.UNIT }; +export type ObjCore = Obj & { type: ObjType.CORE }; + +export interface Game { + elapsed_ticks: number; + config: Config; + my_team_id: number; + objects: Obj[]; +} + +export type GameState = Game; + +export enum ActionType { + CREATE = "create", + MOVE = "move", + ATTACK = "attack", + TRANSFER = "transfer_gems", + BUILD = "build" +} + +export type Action = + | { type: ActionType.CREATE, unit_type: UnitType } + | { type: ActionType.MOVE, unit_id: number, x: number, y: number } + | { type: ActionType.ATTACK, unit_id: number, target_id: number } + | { type: ActionType.TRANSFER, source_id: number, x: number, y: number, amount: number } + | { type: ActionType.BUILD, unit_id: number, x: number, y: number }; diff --git a/bots/ts/hardcore/.coreignore b/bots/ts/hardcore/.coreignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/ts/hardcore/.devcontainer/devcontainer.json b/bots/ts/hardcore/.devcontainer/devcontainer.json new file mode 100644 index 00000000..1f14f001 --- /dev/null +++ b/bots/ts/hardcore/.devcontainer/devcontainer.json @@ -0,0 +1,56 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "my-core-bot", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": "docker-compose.yml", + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "my-core-bot", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/workspace", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + // "ghcr.io/cirolosapio/devcontainers-features/alpine-docker-outside-of-docker:0": {} + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // "initializeCommand": "make -C ${localWorkspaceFolder} update || true", + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + "shutdownAction": "stopCompose", + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + "postCreateCommand": "apt-get update && apt-get install -y curl", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "oven.bun-vscode" + ], + "settings": { + "remote.autoForwardPorts": false, + "security.workspace.trust.enabled": true, + "security.workspace.trust.untrustedFiles": "open", + "security.workspace.trust.emptyWindow": true + } + } + } + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" +} diff --git a/bots/ts/hardcore/.devcontainer/docker-compose.yml b/bots/ts/hardcore/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..d2310594 --- /dev/null +++ b/bots/ts/hardcore/.devcontainer/docker-compose.yml @@ -0,0 +1,18 @@ +services: + my-core-bot: + image: ghcr.io/42core-team/my-core-bot-ts:dev + platform: linux/amd64 + volumes: + - ..:/workspace + - replays:/workspace/replays + stdin_open: true + + visualizer: + image: ghcr.io/42core-team/visualizer:dev + ports: + - 4000:80 + volumes: + - replays:/usr/share/nginx/html/replays + +volumes: + replays: diff --git a/bots/ts/hardcore/.devcontainer/init_docker.bash b/bots/ts/hardcore/.devcontainer/init_docker.bash new file mode 100644 index 00000000..27da212b --- /dev/null +++ b/bots/ts/hardcore/.devcontainer/init_docker.bash @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# **************************************************************************** # +# # +# ::: :::::::: # +# init_docker.sh :+: :+: :+: # +# +:+ +:+ +:+ # +# By: aguiot-- +#+ +:+ +#+ # +# +#+#+#+#+#+ +#+ # +# Created: 2019/11/18 08:17:08 by aguiot-- #+# #+# # +# Updated: 2020/02/20 14:34:42 by aguiot-- ### ########.fr # +# # +# **************************************************************************** # + +# https://github.com/alexandregv/42toolbox + +# Ensure USER variable is set +[ -z "${USER}" ] && export USER=$(whoami) + +################################################################################ + +# Config +docker_destination="/goinfre/$USER/docker" #=> Select docker destination (goinfre is a good choice) + +################################################################################ + +# Colors +COLOR_INFO=$'\033[1;36m' +COLOR_WARNING=$'\033[1;33m' +COLOR_ERROR=$'\033[1;31m' +COLOR_RESET=$'\033[0m' +COLOR_DATE=$'\033[1;37m' + +# Check if the goinfre folder exists +if [ ! -d "/goinfre" ]; then + echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_ERROR}error${COLOR_RESET} Can only install Docker automatically on 42 Macs. The 'goinfre' folder is missing." + exit 1 +fi + +# Kill Docker if started, so it doesn't create files during the process +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Stopping any running Docker processes..." +pkill Docker + +# Unlinks all symlinks, if they are +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Unlinking existing Docker symlinks..." +unlink ~/Library/Containers/com.docker.docker &>/dev/null ;: +unlink ~/Library/Containers/com.docker.helper &>/dev/null ;: +unlink ~/.docker &>/dev/null ;: + +# Delete directories if they were not symlinks +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Removing existing Docker directories..." +rm -rf ~/Library/Containers/com.docker.{docker,helper} ~/.docker &>/dev/null ;: + +# Create destination directories in case they don't already exist +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Creating destination directories for Docker..." +mkdir -p "$docker_destination"/{com.docker.{docker,helper},.docker} + +# Make symlinks +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Creating symlinks for Docker directories..." +ln -sf "$docker_destination"/com.docker.docker ~/Library/Containers/com.docker.docker +ln -sf "$docker_destination"/com.docker.helper ~/Library/Containers/com.docker.helper +ln -sf "$docker_destination"/.docker ~/.docker + +# Start Docker for Mac +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Starting Docker for Mac..." +open -g -a Docker + +# Infinite loop to check if Docker daemon started successfully +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Waiting for Docker daemon to start..." +while true; do + if docker info > /dev/null 2>&1; then + echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Docker daemon started successfully!" + break + else + echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_WARNING}warning${COLOR_RESET} Docker daemon is not running yet. Retrying in 5 seconds..." + sleep 5 + fi +done diff --git a/bots/ts/hardcore/.gitignore b/bots/ts/hardcore/.gitignore new file mode 100644 index 00000000..800872f3 --- /dev/null +++ b/bots/ts/hardcore/.gitignore @@ -0,0 +1,9 @@ +build/ +bot +logs/ +old-logs/ +.DS_Store + +devpod + +replays/replay_latest.json diff --git a/bots/ts/hardcore/Makefile b/bots/ts/hardcore/Makefile new file mode 100644 index 00000000..42f59817 --- /dev/null +++ b/bots/ts/hardcore/Makefile @@ -0,0 +1,204 @@ +CORE_DIR = /core + +PLAYER1_ID := 42 +PLAYER2_ID := 43 + +PLAYER1_BOT := my-core-bot/index.ts +PLAYER2_BOT := gridmaster/index.ts + +PLAYER1_DIR = $(patsubst %/,%,$(dir $(PLAYER1_BOT))) +PLAYER2_DIR = $(patsubst %/,%,$(dir $(PLAYER2_BOT))) + +PLAYER1_BOT_NAME = $(notdir $(PLAYER1_BOT)) +PLAYER2_BOT_NAME = $(notdir $(PLAYER2_BOT)) + + +run: stop check_update_configs + @echo "" + @echo "$(COLOR_INFO)๐ŸŽฎ Game Visualizer is running at: localhost:4000 $(COLOR_RESET)" + @echo "" + $(CORE_DIR)/server /workspace/configs/server.config.json /workspace/configs/game.config.json $(CORE_DIR)/data $(PLAYER1_ID) $(PLAYER2_ID) > /dev/null & + bun run $(PLAYER1_BOT) $(PLAYER1_ID) > /dev/null & + bun run $(PLAYER2_BOT) $(PLAYER2_ID) + @echo "" + @echo "$(COLOR_INFO)๐ŸŽฎ Game Visualizer is running at: localhost:4000 $(COLOR_RESET)" + @echo "" + +check_update_configs: + chmod +x scripts/check_update_configs.sh + -./scripts/check_update_configs.sh + +$(PLAYER1_BOT): + @echo "No build needed for TypeScript" + +$(PLAYER2_BOT): + @echo "No build needed for TypeScript" + + +clean: stop + +fclean: clean + +re: fclean run + +stop: + @pkill server > /dev/null || true & + @pkill bun > /dev/null || true + +update: stop-devcontainer remove-devcontainer + @docker compose --project-directory=./.devcontainer pull + +.PHONY: run clean fclean re stop update $(PLAYER1_BOT) $(PLAYER2_BOT) + + +# Devpod CLI executable +DEVCLI := ./devpod +DEVPOD_ID := $(shell echo "my-core-bot-$(shell basename $(CURDIR) | tr '[:upper:]_' '[:lower:]-')" | cut -c1-40)-$(shell echo "$(CURDIR)" | shasum | cut -c1-7) +# Default to vscode if none is specified +IDE ?= vscode + +.PHONY: all dev-container stop-devcontainer remove-devcontainer + +# Usage: +# make devcontainer (IDE=) (RECREATE=true) +# make stop-devcontainer +# make remove-devcontainer +# +# This command sets up and launches a development container using the Devpod CLI. +# The IDE can be specified using the `IDE` environment variable (default: vscode). +# Ensure Docker is installed and running before executing this command. + +# List of possible IDE values (for reference) +# vscode, openvscode, cursor, zed, codium, intellij, pycharm, phpstorm, +# rider, fleet, goland, webstorm, rustrover, rubymine, clion, dataspell, +# jupyternotebook, vscode-insiders, positron, rstudio, web +# +devcontainer: __print-banner __install-devpod __check-docker __add-docker-provider __launch-devpod + +# Define colors +COLOR_DATE :=\033[1;37m +COLOR_INFO :=\033[1;36m +COLOR_WARNING :=\033[1;33m +COLOR_ERROR :=\033[1;31m +COLOR_RESET :=\033[0m + +__print-banner: + @echo ""; \ + echo "$(COLOR_INFO)===================================================$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| Welcome to your core bot! ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| Setting things up with Devpod CLI ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| be ready ... ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)===================================================$(COLOR_RESET)"; \ + echo "" ; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) ๐Ÿš€ Starting Dev Container Environment"; \ + +__install-devpod: + @if [ ! -f "$(DEVCLI)" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Detecting OS and architecture..."; \ + OS_NAME=$$(uname -s | tr '[:upper:]' '[:lower:]'); \ + OS_ARCH=$$(uname -m); \ + if [ "$$OS_NAME" = "darwin" ]; then \ + if [ "$$OS_ARCH" = "arm64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-darwin-arm64"; \ + elif [ "$$OS_ARCH" = "x86_64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-darwin-amd64"; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Unsupported macOS architecture: $$OS_ARCH"; \ + exit 1; \ + fi; \ + elif [ "$$OS_NAME" = "linux" ]; then \ + if [ "$$OS_ARCH" = "x86_64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-linux-amd64"; \ + elif [ "$$OS_ARCH" = "aarch64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-linux-arm64"; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Unsupported Linux architecture: $$OS_ARCH"; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Unsupported OS: $$OS_NAME"; \ + exit 1; \ + fi; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Installing Devpod CLI for OS='$$OS_NAME' ARCH='$$OS_ARCH'..."; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Downloading from: $$DEV_URL"; \ + curl -L -o "$(DEVCLI)" "$$DEV_URL"; \ + chmod +x "$(DEVCLI)"; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI installed in the current directory."; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI is already installed."; \ + fi + +__check-docker: + @if ! docker info > /dev/null 2>&1; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Docker is not running. Attempting to install and start Docker..."; \ + if [ -f "./.devcontainer/init_docker.bash" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Running init_docker.bash script..."; \ + bash ./.devcontainer/init_docker.bash; \ + if ! docker info > /dev/null 2>&1; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Docker installation or startup failed. Please check the init_docker.bash script."; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) init_docker.bash script not found. Cannot install Docker automatically."; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Docker is running."; \ + fi + +__add-docker-provider: + @if ./devpod provider add docker > /dev/null 2>&1; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Successfully added 'docker' as a provider."; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_WARNING)warning$(COLOR_RESET) The provider 'docker' already exists. If needed, run './devpod provider delete docker'."; \ + fi + +__launch-devpod: + @echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Errors resulting from the docker compose pull cmd can be safely ignored" + @if [ "$(IDE)" = "web" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Launching Devpod in web mode (no local IDE)."; \ + if ! ./devpod up . --id "$(DEVPOD_ID)" $$( [ "$(RECREATE)" = "true" ] && echo "--recreate" ); then \ + echo ""; \ + echo "$(COLOR_ERROR)โŒ Failed to start devcontainer!$(COLOR_RESET)"; \ + echo "$(COLOR_WARNING)โš ๏ธ This is likely due to port conflicts (port 4000 already in use).$(COLOR_RESET)"; \ + echo ""; \ + echo "$(COLOR_INFO)๐Ÿ’ก Try one of these solutions:$(COLOR_RESET)"; \ + echo " 1. Stop other devcontainers: $(COLOR_INFO)make stop-devcontainer$(COLOR_RESET)"; \ + echo " 2. Stop all Docker containers: $(COLOR_INFO)docker stop $$(docker ps -q)$(COLOR_RESET)"; \ + echo " 3. Check what's using port 4000: $(COLOR_INFO)lsof -i :4000$(COLOR_RESET)"; \ + echo " 4. Recreate the devcontainer: $(COLOR_INFO)make devcontainer RECREATE=true$(COLOR_RESET)"; \ + echo ""; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Launching Devpod with IDE: $(IDE)"; \ + if ! ./devpod up . --id "$(DEVPOD_ID)" --ide "$(IDE)" $$( [ "$(RECREATE)" = "true" ] && echo "--recreate" ); then \ + echo ""; \ + echo "$(COLOR_ERROR)โŒ Failed to start devcontainer!$(COLOR_RESET)"; \ + echo "$(COLOR_WARNING)โš ๏ธ This is likely due to port conflicts (port 4000 already in use).$(COLOR_RESET)"; \ + echo ""; \ + echo "$(COLOR_INFO)๐Ÿ’ก Try one of these solutions:$(COLOR_RESET)"; \ + echo " 1. Stop other devcontainers: $(COLOR_INFO)make stop-devcontainer$(COLOR_RESET)"; \ + echo " 2. Stop all Docker containers: $(COLOR_INFO)docker stop $$(docker ps -q)$(COLOR_RESET)"; \ + echo " 3. Check what's using port 4000: $(COLOR_INFO)lsof -i :4000$(COLOR_RESET)"; \ + echo " 4. Recreate the devcontainer: $(COLOR_INFO)make devcontainer RECREATE=true$(COLOR_RESET)"; \ + echo ""; \ + exit 1; \ + fi; \ + fi + +stop-devcontainer: + @./devpod stop $(DEVPOD_ID) || echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_WARNING)warning$(COLOR_RESET) No running Devpod containers found." + +remove-devcontainer: + @./devpod delete $(DEVPOD_ID) || echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_WARNING)warning$(COLOR_RESET) No Devpod containers found to remove." + +uninstall-devpod: + @if [ -f "$(DEVCLI)" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Uninstalling Devpod CLI..."; \ + rm -f "$(DEVCLI)"; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI has been uninstalled."; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI is not installed."; \ + fi diff --git a/bots/ts/hardcore/README.md b/bots/ts/hardcore/README.md new file mode 100644 index 00000000..5d7abf2c --- /dev/null +++ b/bots/ts/hardcore/README.md @@ -0,0 +1,64 @@ +

+ CORE Logo +

+ +# ๐ŸŒŸ CORE REPO + +## ๐ŸŽ‰ Good Luck, Have Fun, and [RTFM](https://coregame.sh/wiki/)!1!!1 ๐Ÿš€ + +Welcome to the **CORE** project repository! Weโ€™re excited to have you on board for this coding adventure. + +### ๐Ÿš€ Quick Start Guide + +1. Clone the repository and set up your dev container: + ```bash + git clone your-repo-url && cd my-core-bot && make devcontainer + ``` +2. Run `make` in the terminal to test. +3. Open [localhost:4000](http://localhost:4000) in your browser to see the gameplay. +4. Keep going writing your bot! Get started under `my-core-bot/src/main.c`! + +### ๐Ÿ“š Useful Links +- **Official CORE Wiki**: [coregame.sh/wiki](https://coregame.sh/wiki) + +### ๐Ÿ› ๏ธ Spin Up Your Dev Container + +Want to get hacking right away? Set up your dev environment in one simple command using [Devpod](https://devpod.sh/)! ๐Ÿš€ + +```bash +make devcontainer +``` + +This command will: +1. Automatically download and install the **Devpod CLI** (if itโ€™s not already there). +2. Ensure **Docker** is up and running (it will attempt to start Docker on 42 iMacs if itโ€™s not started). +3. Set up the **Docker provider** for Devpod. +4. Launch your preferred IDE inside a fully configured **Dev Container** + +> ๐Ÿ’ก **Tip**: You can specify your favorite IDE by passing the `IDE` variable. For example: +> ```bash +> make devcontainer IDE=zed +> ``` + +๐Ÿ“‹ **Default IDE**: `vscode` +๐Ÿงฐ **Supported IDEs**: `vscode`, `openvscode`, `cursor`, `zed`, `codium`, `intellij`, `pycharm`, `phpstorm`, +`rider`, `fleet`, `goland`, `webstorm`, `rustrover`, `rubymine`, `clion`, `dataspell`, `jupyternotebook`, +`vscode-insiders`, `positron`, `rstudio` + +#### ๐Ÿ›‘ Stop the Dev Container +To stop the running Dev Container, use: +```bash +make stop-devcontainer +``` +This will stop the container without removing it, allowing you to restart it later. + +#### โŒ Remove the Dev Container +To completely remove the Dev Container, use: +```bash +make remove-devcontainer +``` +This will delete the container and its associated resources. + +> โ“ **QnA**: Why can't I see all of the files? +> Some files are hidden by VSCode. These files are generally not relevant. If you want to see all of them, follow the guide on how to show them in the FAQ page on the wiki. + diff --git a/bots/ts/hardcore/gridmaster/index.ts b/bots/ts/hardcore/gridmaster/index.ts new file mode 100644 index 00000000..a46ac4a0 --- /dev/null +++ b/bots/ts/hardcore/gridmaster/index.ts @@ -0,0 +1,33 @@ +import { ClientLib } from '../../../core/ts/client_lib/client_lib'; +import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; + +const teamId = process.argv.length >= 3 ? Number(process.argv[2]) : 0; +if (isNaN(teamId)) { + console.error("Invalid team id. Usage: ./bot "); + process.exit(1); +} + +const host = process.env.SERVER_IP || '127.0.0.1'; +const port = process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT) : 4444; +const bot = new ClientLib("Hardcore Gridmaster TS", teamId, true); + +bot.startGame((game: GameState) => { + // Create a warrior + bot.createUnit(UnitType.WARRIOR); + + // Find opponent core + const opponentCore = game.objects.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + ); + + if (opponentCore) { + // Move all own units to opponent core + game.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { + bot.move(obj, opponentCore.pos); + } + }); + } +}); + +bot.connect(); diff --git a/bots/ts/hardcore/gridmaster/package.json b/bots/ts/hardcore/gridmaster/package.json new file mode 100644 index 00000000..1d5f8e98 --- /dev/null +++ b/bots/ts/hardcore/gridmaster/package.json @@ -0,0 +1,12 @@ +{ + "name": "hardcore-gridmaster", + "version": "1.0.0", + "description": "Hardcore Gridmaster TS bot", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@core/client-lib": "file:../../../core/ts/client_lib" + } +} diff --git a/bots/ts/hardcore/my-core-bot/index.ts b/bots/ts/hardcore/my-core-bot/index.ts new file mode 100644 index 00000000..b55a1544 --- /dev/null +++ b/bots/ts/hardcore/my-core-bot/index.ts @@ -0,0 +1,32 @@ +import { ClientLib } from '../../../core/ts/client_lib/client_lib'; +import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; + +const teamId = process.argv.length >= 3 ? Number(process.argv[2]) : 0; +if (isNaN(teamId)) { + console.error("Invalid team id. Usage: ./bot "); + process.exit(1); +} + +const host = process.env.SERVER_IP || '127.0.0.1'; +const port = process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT) : 4444; +const bot = new ClientLib("Hardcore TS Core Bot", teamId, true); + +bot.startGame((game: GameState) => { + // Create a warrior + bot.createUnit(UnitType.WARRIOR); + + // Hardcore bot logic + const opponentCore = game.objects.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + ); + + if (opponentCore) { + game.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { + bot.move(obj, opponentCore.pos); + } + }); + } +}); + +bot.connect(); diff --git a/bots/ts/hardcore/my-core-bot/package.json b/bots/ts/hardcore/my-core-bot/package.json new file mode 100644 index 00000000..e1531185 --- /dev/null +++ b/bots/ts/hardcore/my-core-bot/package.json @@ -0,0 +1,12 @@ +{ + "name": "hardcore-my-core-bot", + "version": "1.0.0", + "description": "Hardcore TS Core Bot", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@core/client-lib": "file:../../../core/ts/client_lib" + } +} diff --git a/bots/ts/hardcore/package.json b/bots/ts/hardcore/package.json new file mode 100644 index 00000000..67d90022 --- /dev/null +++ b/bots/ts/hardcore/package.json @@ -0,0 +1,14 @@ +{ + "name": "hardcore-ts", + "version": "1.0.0", + "description": "Hardcore TypeScript bot for Core game", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts", + "gridmaster": "bun run gridmaster/index.ts", + "my-core-bot": "bun run my-core-bot/index.ts" + }, + "dependencies": { + "@types/node": "^20.11.0" + } +} diff --git a/bots/ts/hardcore/replays/file to keep folder in git b/bots/ts/hardcore/replays/file to keep folder in git new file mode 100644 index 00000000..e69de29b diff --git a/bots/ts/hardcore/scripts/check_image_updates.sh b/bots/ts/hardcore/scripts/check_image_updates.sh new file mode 100755 index 00000000..de1fc05a --- /dev/null +++ b/bots/ts/hardcore/scripts/check_image_updates.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Configuration +URL="[[event_url]]/version" +MAX_TIME=0.5 +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Determine paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +COMPOSE_FILE="$ROOT_DIR/.devcontainer/docker-compose.yml" + +# Check if compose file exists +if [ ! -f "$COMPOSE_FILE" ]; then + # If not found, just skip without error + exit 0 +fi + +# Fetch versions from API with timeout +# Using -s for silent, --max-time for timeout +RESPONSE=$(curl -s --max-time "$MAX_TIME" "$URL" 2>/dev/null) + +# Check if response is empty (timeout or error) +if [ -z "$RESPONSE" ]; then + exit 0 +fi + +get_json_value() { + local key=$1 + local json=$2 + + echo "$json" | yq eval ".$key" - +} + +NEW_BOT_VER=$(get_json_value "myCoreBotVersion" "$RESPONSE") +NEW_VIS_VER=$(get_json_value "visualizerVersion" "$RESPONSE") + +# Validate versions to prevent injection +validate_version() { + local version=$1 + if [[ ! "$version" =~ ^[a-zA-Z0-9._:/-]+$ ]]; then + return 1 + fi + return 0 +} + +if [ -n "$NEW_BOT_VER" ] && [ "$NEW_BOT_VER" != "null" ]; then + if ! validate_version "$NEW_BOT_VER"; then + echo "Error: Invalid characters detected in new bot version: '$NEW_BOT_VER'. Update aborted." >&2 + exit 1 + fi +fi + +if [ -n "$NEW_VIS_VER" ] && [ "$NEW_VIS_VER" != "null" ]; then + if ! validate_version "$NEW_VIS_VER"; then + echo "Error: Invalid characters detected in new visualizer version: '$NEW_VIS_VER'. Update aborted." >&2 + exit 1 + fi +fi + +get_current_image() { + local service_name=$1 + yq eval ".services[\"$service_name\"].image" "$COMPOSE_FILE" +} + +CURRENT_BOT_VER=$(get_current_image "my-core-bot") +CURRENT_VIS_VER=$(get_current_image "visualizer") + +UPDATES_FOUND=false +BOT_NEEDS_UPDATE=false +VIS_NEEDS_UPDATE=false + +# Helper to check if value is valid (non-empty and not "null") +is_valid() { + [ -n "$1" ] && [ "$1" != "null" ] +} + +if is_valid "$NEW_BOT_VER" && is_valid "$CURRENT_BOT_VER" && [ "$CURRENT_BOT_VER" != "$NEW_BOT_VER" ]; then + UPDATES_FOUND=true + BOT_NEEDS_UPDATE=true +fi + +if is_valid "$NEW_VIS_VER" && is_valid "$CURRENT_VIS_VER" && [ "$CURRENT_VIS_VER" != "$NEW_VIS_VER" ]; then + UPDATES_FOUND=true + VIS_NEEDS_UPDATE=true +fi + +if [ "$UPDATES_FOUND" = false ]; then + exit 0 +fi + +# Prompt user +echo "" +echo "New Docker image versions available!" +if [ "$BOT_NEEDS_UPDATE" = true ]; then + echo " - My Core Bot: $CURRENT_BOT_VER -> $NEW_BOT_VER" +fi +if [ "$VIS_NEEDS_UPDATE" = true ]; then + echo " - Visualizer: $CURRENT_VIS_VER -> $NEW_VIS_VER" +fi +echo "" +echo -e "${RED}โš ๏ธ WARNING: You are running an outdated version. To ensure your bot remains functional on the website and in tournaments, please update to the latest version immediately.${NC}" +printf "Do you want to update? (yes/no): " +read -r USER_INPUT + +if [ "$USER_INPUT" = "yes" ]; then + # Perform updates + + if [ "$BOT_NEEDS_UPDATE" = true ]; then + echo "Updating 'my-core-bot' image to: $NEW_BOT_VER" + yq eval ".services[\"my-core-bot\"].image = \"$NEW_BOT_VER\"" -i "$COMPOSE_FILE" + fi + + if [ "$VIS_NEEDS_UPDATE" = true ]; then + echo "Updating 'visualizer' image to: $NEW_VIS_VER" + yq eval ".services[\"visualizer\"].image = \"$NEW_VIS_VER\"" -i "$COMPOSE_FILE" + fi + + echo "" + echo "Updated docker-compose.yml" + echo -e "${RED}Please close the current devcontainer. Then, from your local terminal in the bot folder, run this command to update and restart:${NC}" + echo "make update && make devcontainer" + echo "" + echo "Note: Please also commit the changes of the docker-compose.yml file!" + echo "" + + exit 1 +else + exit 0 +fi diff --git a/bots/ts/hardcore/scripts/check_update_configs.sh b/bots/ts/hardcore/scripts/check_update_configs.sh new file mode 100644 index 00000000..b38c264c --- /dev/null +++ b/bots/ts/hardcore/scripts/check_update_configs.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +check_config() { + local config_name="$1" + local local_path="configs/${config_name}.config.json" + local url="[[event_url]]/${config_name}-config" + + local remote_content + remote_content=$(curl -sf --max-time 1 "$url") || { + echo "Failed to fetch: $url - this might mean you are playing with an out of date config. You should address this." >&2 + return 1 + } + + local local_content + local_content=$(<"$local_path") + + if [[ "$remote_content" == "$local_content" ]]; then + return 0 + fi + + echo "" + echo "==================================================" + echo "โš  CONFIG CHANGED: ${config_name}.config.json" + echo "==================================================" + echo "" + + local backup_path="${local_path%.*}_old.${local_path##*.}" + + cp "$local_path" "$backup_path" + echo "$remote_content" > "$local_path" + + echo "The following changes have occurred:" + diff --color=auto -u "$backup_path" "$local_path" || true +} + +check_config "game" +check_config "server" diff --git a/bots/ts/softcore/.coreignore b/bots/ts/softcore/.coreignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/ts/softcore/.devcontainer/devcontainer.json b/bots/ts/softcore/.devcontainer/devcontainer.json new file mode 100644 index 00000000..1f14f001 --- /dev/null +++ b/bots/ts/softcore/.devcontainer/devcontainer.json @@ -0,0 +1,56 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "my-core-bot", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": "docker-compose.yml", + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "my-core-bot", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/workspace", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + // "ghcr.io/cirolosapio/devcontainers-features/alpine-docker-outside-of-docker:0": {} + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // "initializeCommand": "make -C ${localWorkspaceFolder} update || true", + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + "shutdownAction": "stopCompose", + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + "postCreateCommand": "apt-get update && apt-get install -y curl", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "oven.bun-vscode" + ], + "settings": { + "remote.autoForwardPorts": false, + "security.workspace.trust.enabled": true, + "security.workspace.trust.untrustedFiles": "open", + "security.workspace.trust.emptyWindow": true + } + } + } + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" +} diff --git a/bots/ts/softcore/.devcontainer/docker-compose.yml b/bots/ts/softcore/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..d2310594 --- /dev/null +++ b/bots/ts/softcore/.devcontainer/docker-compose.yml @@ -0,0 +1,18 @@ +services: + my-core-bot: + image: ghcr.io/42core-team/my-core-bot-ts:dev + platform: linux/amd64 + volumes: + - ..:/workspace + - replays:/workspace/replays + stdin_open: true + + visualizer: + image: ghcr.io/42core-team/visualizer:dev + ports: + - 4000:80 + volumes: + - replays:/usr/share/nginx/html/replays + +volumes: + replays: diff --git a/bots/ts/softcore/.devcontainer/init_docker.bash b/bots/ts/softcore/.devcontainer/init_docker.bash new file mode 100644 index 00000000..27da212b --- /dev/null +++ b/bots/ts/softcore/.devcontainer/init_docker.bash @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# **************************************************************************** # +# # +# ::: :::::::: # +# init_docker.sh :+: :+: :+: # +# +:+ +:+ +:+ # +# By: aguiot-- +#+ +:+ +#+ # +# +#+#+#+#+#+ +#+ # +# Created: 2019/11/18 08:17:08 by aguiot-- #+# #+# # +# Updated: 2020/02/20 14:34:42 by aguiot-- ### ########.fr # +# # +# **************************************************************************** # + +# https://github.com/alexandregv/42toolbox + +# Ensure USER variable is set +[ -z "${USER}" ] && export USER=$(whoami) + +################################################################################ + +# Config +docker_destination="/goinfre/$USER/docker" #=> Select docker destination (goinfre is a good choice) + +################################################################################ + +# Colors +COLOR_INFO=$'\033[1;36m' +COLOR_WARNING=$'\033[1;33m' +COLOR_ERROR=$'\033[1;31m' +COLOR_RESET=$'\033[0m' +COLOR_DATE=$'\033[1;37m' + +# Check if the goinfre folder exists +if [ ! -d "/goinfre" ]; then + echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_ERROR}error${COLOR_RESET} Can only install Docker automatically on 42 Macs. The 'goinfre' folder is missing." + exit 1 +fi + +# Kill Docker if started, so it doesn't create files during the process +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Stopping any running Docker processes..." +pkill Docker + +# Unlinks all symlinks, if they are +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Unlinking existing Docker symlinks..." +unlink ~/Library/Containers/com.docker.docker &>/dev/null ;: +unlink ~/Library/Containers/com.docker.helper &>/dev/null ;: +unlink ~/.docker &>/dev/null ;: + +# Delete directories if they were not symlinks +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Removing existing Docker directories..." +rm -rf ~/Library/Containers/com.docker.{docker,helper} ~/.docker &>/dev/null ;: + +# Create destination directories in case they don't already exist +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Creating destination directories for Docker..." +mkdir -p "$docker_destination"/{com.docker.{docker,helper},.docker} + +# Make symlinks +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Creating symlinks for Docker directories..." +ln -sf "$docker_destination"/com.docker.docker ~/Library/Containers/com.docker.docker +ln -sf "$docker_destination"/com.docker.helper ~/Library/Containers/com.docker.helper +ln -sf "$docker_destination"/.docker ~/.docker + +# Start Docker for Mac +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Starting Docker for Mac..." +open -g -a Docker + +# Infinite loop to check if Docker daemon started successfully +echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Waiting for Docker daemon to start..." +while true; do + if docker info > /dev/null 2>&1; then + echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_INFO}info${COLOR_RESET} Docker daemon started successfully!" + break + else + echo "${COLOR_DATE}$(date +'%H:%M:%S')${COLOR_RESET} ${COLOR_WARNING}warning${COLOR_RESET} Docker daemon is not running yet. Retrying in 5 seconds..." + sleep 5 + fi +done diff --git a/bots/ts/softcore/.gitignore b/bots/ts/softcore/.gitignore new file mode 100644 index 00000000..800872f3 --- /dev/null +++ b/bots/ts/softcore/.gitignore @@ -0,0 +1,9 @@ +build/ +bot +logs/ +old-logs/ +.DS_Store + +devpod + +replays/replay_latest.json diff --git a/bots/ts/softcore/Makefile b/bots/ts/softcore/Makefile new file mode 100644 index 00000000..f186b40d --- /dev/null +++ b/bots/ts/softcore/Makefile @@ -0,0 +1,206 @@ +CORE_DIR = /core + +PLAYER1_ID := 42 +PLAYER2_ID := 43 + +PLAYER1_BOT := my-core-bot/index.ts +PLAYER2_BOT := gridmaster/index.ts + +PLAYER1_DIR = $(patsubst %/,%,$(dir $(PLAYER1_BOT))) +PLAYER2_DIR = $(patsubst %/,%,$(dir $(PLAYER2_BOT))) + +PLAYER1_BOT_NAME = $(notdir $(PLAYER1_BOT)) +PLAYER2_BOT_NAME = $(notdir $(PLAYER2_BOT)) + + +run: stop check_update_configs + @echo "" + @echo "$(COLOR_INFO)๐ŸŽฎ Game Visualizer is running at: localhost:4000 $(COLOR_RESET)" + @echo "" + $(CORE_DIR)/server /workspace/configs/server.config.json /workspace/configs/game.config.json $(CORE_DIR)/data $(PLAYER1_ID) $(PLAYER2_ID) > /dev/null & + bun run $(PLAYER1_BOT) $(PLAYER1_ID) > /dev/null & + bun run $(PLAYER2_BOT) $(PLAYER2_ID) + @echo "" + @echo "$(COLOR_INFO)๐ŸŽฎ Game Visualizer is running at: localhost:4000 $(COLOR_RESET)" + @echo "" + +check_updates: + @chmod +x ./scripts/check_update_configs.sh + -./scripts/check_update_configs.sh + @chmod +x ./scripts/check_image_updates.sh + ./scripts/check_image_updates.sh + +$(PLAYER1_BOT): + @echo "No build needed for TypeScript" + +$(PLAYER2_BOT): + @echo "No build needed for TypeScript" + + +clean: stop + +fclean: clean + +re: fclean run + +stop: + @pkill server > /dev/null || true & + @pkill bun > /dev/null || true + +update: stop-devcontainer remove-devcontainer + @docker compose --project-directory=./.devcontainer pull + +.PHONY: run clean fclean re stop update $(PLAYER1_BOT) $(PLAYER2_BOT) + + +# Devpod CLI executable +DEVCLI := ./devpod +DEVPOD_ID := $(shell echo "my-core-bot-$(shell basename $(CURDIR) | tr '[:upper:]_' '[:lower:]-')" | cut -c1-40)-$(shell echo "$(CURDIR)" | shasum | cut -c1-7) +# Default to vscode if none is specified +IDE ?= vscode + +.PHONY: all dev-container stop-devcontainer remove-devcontainer + +# Usage: +# make devcontainer (IDE=) (RECREATE=true) +# make stop-devcontainer +# make remove-devcontainer +# +# This command sets up and launches a development container using the Devpod CLI. +# The IDE can be specified using the `IDE` environment variable (default: vscode). +# Ensure Docker is installed and running before executing this command. + +# List of possible IDE values (for reference) +# vscode, openvscode, cursor, zed, codium, intellij, pycharm, phpstorm, +# rider, fleet, goland, webstorm, rustrover, rubymine, clion, dataspell, +# jupyternotebook, vscode-insiders, positron, rstudio, web +# +devcontainer: __print-banner __install-devpod __check-docker __add-docker-provider __launch-devpod + +# Define colors +COLOR_DATE :=\033[1;37m +COLOR_INFO :=\033[1;36m +COLOR_WARNING :=\033[1;33m +COLOR_ERROR :=\033[1;31m +COLOR_RESET :=\033[0m + +__print-banner: + @echo ""; \ + echo "$(COLOR_INFO)===================================================$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| Welcome to your core bot! ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| Setting things up with Devpod CLI ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)|| be ready ... ||$(COLOR_RESET)"; \ + echo "$(COLOR_INFO)===================================================$(COLOR_RESET)"; \ + echo "" ; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) ๐Ÿš€ Starting Dev Container Environment"; \ + +__install-devpod: + @if [ ! -f "$(DEVCLI)" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Detecting OS and architecture..."; \ + OS_NAME=$$(uname -s | tr '[:upper:]' '[:lower:]'); \ + OS_ARCH=$$(uname -m); \ + if [ "$$OS_NAME" = "darwin" ]; then \ + if [ "$$OS_ARCH" = "arm64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-darwin-arm64"; \ + elif [ "$$OS_ARCH" = "x86_64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-darwin-amd64"; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Unsupported macOS architecture: $$OS_ARCH"; \ + exit 1; \ + fi; \ + elif [ "$$OS_NAME" = "linux" ]; then \ + if [ "$$OS_ARCH" = "x86_64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-linux-amd64"; \ + elif [ "$$OS_ARCH" = "aarch64" ]; then \ + DEV_URL="https://github.com/loft-sh/devpod/releases/latest/download/devpod-linux-arm64"; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Unsupported Linux architecture: $$OS_ARCH"; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Unsupported OS: $$OS_NAME"; \ + exit 1; \ + fi; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Installing Devpod CLI for OS='$$OS_NAME' ARCH='$$OS_ARCH'..."; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Downloading from: $$DEV_URL"; \ + curl -L -o "$(DEVCLI)" "$$DEV_URL"; \ + chmod +x "$(DEVCLI)"; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI installed in the current directory."; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI is already installed."; \ + fi + +__check-docker: + @if ! docker info > /dev/null 2>&1; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Docker is not running. Attempting to install and start Docker..."; \ + if [ -f "./.devcontainer/init_docker.bash" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Running init_docker.bash script..."; \ + bash ./.devcontainer/init_docker.bash; \ + if ! docker info > /dev/null 2>&1; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) Docker installation or startup failed. Please check the init_docker.bash script."; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_ERROR)error$(COLOR_RESET) init_docker.bash script not found. Cannot install Docker automatically."; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Docker is running."; \ + fi + +__add-docker-provider: + @if ./devpod provider add docker > /dev/null 2>&1; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Successfully added 'docker' as a provider."; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_WARNING)warning$(COLOR_RESET) The provider 'docker' already exists. If needed, run './devpod provider delete docker'."; \ + fi + +__launch-devpod: + @echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Errors resulting from the docker compose pull cmd can be safely ignored" + @if [ "$(IDE)" = "web" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Launching Devpod in web mode (no local IDE)."; \ + if ! ./devpod up . --id "$(DEVPOD_ID)" $$( [ "$(RECREATE)" = "true" ] && echo "--recreate" ); then \ + echo ""; \ + echo "$(COLOR_ERROR)โŒ Failed to start devcontainer!$(COLOR_RESET)"; \ + echo "$(COLOR_WARNING)โš ๏ธ This is likely due to port conflicts (port 4000 already in use).$(COLOR_RESET)"; \ + echo ""; \ + echo "$(COLOR_INFO)๐Ÿ’ก Try one of these solutions:$(COLOR_RESET)"; \ + echo " 1. Stop other devcontainers: $(COLOR_INFO)make stop-devcontainer$(COLOR_RESET)"; \ + echo " 2. Stop all Docker containers: $(COLOR_INFO)docker stop $$(docker ps -q)$(COLOR_RESET)"; \ + echo " 3. Check what's using port 4000: $(COLOR_INFO)lsof -i :4000$(COLOR_RESET)"; \ + echo " 4. Recreate the devcontainer: $(COLOR_INFO)make devcontainer RECREATE=true$(COLOR_RESET)"; \ + echo ""; \ + exit 1; \ + fi; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Launching Devpod with IDE: $(IDE)"; \ + if ! ./devpod up . --id "$(DEVPOD_ID)" --ide "$(IDE)" $$( [ "$(RECREATE)" = "true" ] && echo "--recreate" ); then \ + echo ""; \ + echo "$(COLOR_ERROR)โŒ Failed to start devcontainer!$(COLOR_RESET)"; \ + echo "$(COLOR_WARNING)โš ๏ธ This is likely due to port conflicts (port 4000 already in use).$(COLOR_RESET)"; \ + echo ""; \ + echo "$(COLOR_INFO)๐Ÿ’ก Try one of these solutions:$(COLOR_RESET)"; \ + echo " 1. Stop other devcontainers: $(COLOR_INFO)make stop-devcontainer$(COLOR_RESET)"; \ + echo " 2. Stop all Docker containers: $(COLOR_INFO)docker stop $$(docker ps -q)$(COLOR_RESET)"; \ + echo " 3. Check what's using port 4000: $(COLOR_INFO)lsof -i :4000$(COLOR_RESET)"; \ + echo " 4. Recreate the devcontainer: $(COLOR_INFO)make devcontainer RECREATE=true$(COLOR_RESET)"; \ + echo ""; \ + exit 1; \ + fi; \ + fi + +stop-devcontainer: + @./devpod stop $(DEVPOD_ID) || echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_WARNING)warning$(COLOR_RESET) No running Devpod containers found." + +remove-devcontainer: + @./devpod delete $(DEVPOD_ID) || echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_WARNING)warning$(COLOR_RESET) No Devpod containers found to remove." + +uninstall-devpod: + @if [ -f "$(DEVCLI)" ]; then \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Uninstalling Devpod CLI..."; \ + rm -f "$(DEVCLI)"; \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI has been uninstalled."; \ + else \ + echo "$(COLOR_DATE)$$(date +'%H:%M:%S')$(COLOR_RESET) $(COLOR_INFO)info$(COLOR_RESET) Devpod CLI is not installed."; \ + fi diff --git a/bots/ts/softcore/README.md b/bots/ts/softcore/README.md new file mode 100644 index 00000000..5d7abf2c --- /dev/null +++ b/bots/ts/softcore/README.md @@ -0,0 +1,64 @@ +

+ CORE Logo +

+ +# ๐ŸŒŸ CORE REPO + +## ๐ŸŽ‰ Good Luck, Have Fun, and [RTFM](https://coregame.sh/wiki/)!1!!1 ๐Ÿš€ + +Welcome to the **CORE** project repository! Weโ€™re excited to have you on board for this coding adventure. + +### ๐Ÿš€ Quick Start Guide + +1. Clone the repository and set up your dev container: + ```bash + git clone your-repo-url && cd my-core-bot && make devcontainer + ``` +2. Run `make` in the terminal to test. +3. Open [localhost:4000](http://localhost:4000) in your browser to see the gameplay. +4. Keep going writing your bot! Get started under `my-core-bot/src/main.c`! + +### ๐Ÿ“š Useful Links +- **Official CORE Wiki**: [coregame.sh/wiki](https://coregame.sh/wiki) + +### ๐Ÿ› ๏ธ Spin Up Your Dev Container + +Want to get hacking right away? Set up your dev environment in one simple command using [Devpod](https://devpod.sh/)! ๐Ÿš€ + +```bash +make devcontainer +``` + +This command will: +1. Automatically download and install the **Devpod CLI** (if itโ€™s not already there). +2. Ensure **Docker** is up and running (it will attempt to start Docker on 42 iMacs if itโ€™s not started). +3. Set up the **Docker provider** for Devpod. +4. Launch your preferred IDE inside a fully configured **Dev Container** + +> ๐Ÿ’ก **Tip**: You can specify your favorite IDE by passing the `IDE` variable. For example: +> ```bash +> make devcontainer IDE=zed +> ``` + +๐Ÿ“‹ **Default IDE**: `vscode` +๐Ÿงฐ **Supported IDEs**: `vscode`, `openvscode`, `cursor`, `zed`, `codium`, `intellij`, `pycharm`, `phpstorm`, +`rider`, `fleet`, `goland`, `webstorm`, `rustrover`, `rubymine`, `clion`, `dataspell`, `jupyternotebook`, +`vscode-insiders`, `positron`, `rstudio` + +#### ๐Ÿ›‘ Stop the Dev Container +To stop the running Dev Container, use: +```bash +make stop-devcontainer +``` +This will stop the container without removing it, allowing you to restart it later. + +#### โŒ Remove the Dev Container +To completely remove the Dev Container, use: +```bash +make remove-devcontainer +``` +This will delete the container and its associated resources. + +> โ“ **QnA**: Why can't I see all of the files? +> Some files are hidden by VSCode. These files are generally not relevant. If you want to see all of them, follow the guide on how to show them in the FAQ page on the wiki. + diff --git a/bots/ts/softcore/gridmaster/index.ts b/bots/ts/softcore/gridmaster/index.ts new file mode 100644 index 00000000..9a0db800 --- /dev/null +++ b/bots/ts/softcore/gridmaster/index.ts @@ -0,0 +1,25 @@ +import { ClientLib } from '../../../core/ts/client_lib/client_lib'; +import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; + +const bot = new ClientLib("Gridmaster TS", 1); + +bot.startGame((game: GameState) => { + // Create a warrior + bot.createUnit(UnitType.WARRIOR); + + // Find opponent core + const opponentCore = game.objects.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + ); + + if (opponentCore) { + // Move all own units to opponent core + game.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { + bot.move(obj, opponentCore.pos); + } + }); + } +}); + +bot.connect(); diff --git a/bots/ts/softcore/gridmaster/package.json b/bots/ts/softcore/gridmaster/package.json new file mode 100644 index 00000000..55da2b8a --- /dev/null +++ b/bots/ts/softcore/gridmaster/package.json @@ -0,0 +1,12 @@ +{ + "name": "softcore-gridmaster", + "version": "1.0.0", + "description": "Softcore Gridmaster TS bot", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@core/client-lib": "file:../../../core/ts/client_lib" + } +} diff --git a/bots/ts/softcore/my-core-bot/index.ts b/bots/ts/softcore/my-core-bot/index.ts new file mode 100644 index 00000000..16653cc1 --- /dev/null +++ b/bots/ts/softcore/my-core-bot/index.ts @@ -0,0 +1,24 @@ +import { ClientLib } from '../../../core/ts/client_lib/client_lib'; +import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; + +const bot = new ClientLib("TS Core Bot", 1); + +bot.startGame((game: GameState) => { + // Create a warrior + bot.createUnit(UnitType.WARRIOR); + + // Simple bot logic + const opponentCore = game.objects.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + ); + + if (opponentCore) { + game.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { + bot.move(obj, opponentCore.pos); + } + }); + } +}); + +bot.connect(); diff --git a/bots/ts/softcore/my-core-bot/package.json b/bots/ts/softcore/my-core-bot/package.json new file mode 100644 index 00000000..4470241a --- /dev/null +++ b/bots/ts/softcore/my-core-bot/package.json @@ -0,0 +1,12 @@ +{ + "name": "softcore-my-core-bot", + "version": "1.0.0", + "description": "Softcore TS Core Bot", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@core/client-lib": "file:../../../core/ts/client_lib" + } +} diff --git a/bots/ts/softcore/package.json b/bots/ts/softcore/package.json new file mode 100644 index 00000000..1e19ffa0 --- /dev/null +++ b/bots/ts/softcore/package.json @@ -0,0 +1,14 @@ +{ + "name": "softcore-ts", + "version": "1.0.0", + "description": "Softcore TypeScript bot for Core game", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts", + "gridmaster": "bun run gridmaster/index.ts", + "my-core-bot": "bun run my-core-bot/index.ts" + }, + "dependencies": { + "@types/node": "^20.11.0" + } +} diff --git a/bots/ts/softcore/replays/file to keep folder in git b/bots/ts/softcore/replays/file to keep folder in git new file mode 100644 index 00000000..e69de29b diff --git a/bots/ts/softcore/scripts/check_image_updates.sh b/bots/ts/softcore/scripts/check_image_updates.sh new file mode 100755 index 00000000..de1fc05a --- /dev/null +++ b/bots/ts/softcore/scripts/check_image_updates.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Configuration +URL="[[event_url]]/version" +MAX_TIME=0.5 +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Determine paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +COMPOSE_FILE="$ROOT_DIR/.devcontainer/docker-compose.yml" + +# Check if compose file exists +if [ ! -f "$COMPOSE_FILE" ]; then + # If not found, just skip without error + exit 0 +fi + +# Fetch versions from API with timeout +# Using -s for silent, --max-time for timeout +RESPONSE=$(curl -s --max-time "$MAX_TIME" "$URL" 2>/dev/null) + +# Check if response is empty (timeout or error) +if [ -z "$RESPONSE" ]; then + exit 0 +fi + +get_json_value() { + local key=$1 + local json=$2 + + echo "$json" | yq eval ".$key" - +} + +NEW_BOT_VER=$(get_json_value "myCoreBotVersion" "$RESPONSE") +NEW_VIS_VER=$(get_json_value "visualizerVersion" "$RESPONSE") + +# Validate versions to prevent injection +validate_version() { + local version=$1 + if [[ ! "$version" =~ ^[a-zA-Z0-9._:/-]+$ ]]; then + return 1 + fi + return 0 +} + +if [ -n "$NEW_BOT_VER" ] && [ "$NEW_BOT_VER" != "null" ]; then + if ! validate_version "$NEW_BOT_VER"; then + echo "Error: Invalid characters detected in new bot version: '$NEW_BOT_VER'. Update aborted." >&2 + exit 1 + fi +fi + +if [ -n "$NEW_VIS_VER" ] && [ "$NEW_VIS_VER" != "null" ]; then + if ! validate_version "$NEW_VIS_VER"; then + echo "Error: Invalid characters detected in new visualizer version: '$NEW_VIS_VER'. Update aborted." >&2 + exit 1 + fi +fi + +get_current_image() { + local service_name=$1 + yq eval ".services[\"$service_name\"].image" "$COMPOSE_FILE" +} + +CURRENT_BOT_VER=$(get_current_image "my-core-bot") +CURRENT_VIS_VER=$(get_current_image "visualizer") + +UPDATES_FOUND=false +BOT_NEEDS_UPDATE=false +VIS_NEEDS_UPDATE=false + +# Helper to check if value is valid (non-empty and not "null") +is_valid() { + [ -n "$1" ] && [ "$1" != "null" ] +} + +if is_valid "$NEW_BOT_VER" && is_valid "$CURRENT_BOT_VER" && [ "$CURRENT_BOT_VER" != "$NEW_BOT_VER" ]; then + UPDATES_FOUND=true + BOT_NEEDS_UPDATE=true +fi + +if is_valid "$NEW_VIS_VER" && is_valid "$CURRENT_VIS_VER" && [ "$CURRENT_VIS_VER" != "$NEW_VIS_VER" ]; then + UPDATES_FOUND=true + VIS_NEEDS_UPDATE=true +fi + +if [ "$UPDATES_FOUND" = false ]; then + exit 0 +fi + +# Prompt user +echo "" +echo "New Docker image versions available!" +if [ "$BOT_NEEDS_UPDATE" = true ]; then + echo " - My Core Bot: $CURRENT_BOT_VER -> $NEW_BOT_VER" +fi +if [ "$VIS_NEEDS_UPDATE" = true ]; then + echo " - Visualizer: $CURRENT_VIS_VER -> $NEW_VIS_VER" +fi +echo "" +echo -e "${RED}โš ๏ธ WARNING: You are running an outdated version. To ensure your bot remains functional on the website and in tournaments, please update to the latest version immediately.${NC}" +printf "Do you want to update? (yes/no): " +read -r USER_INPUT + +if [ "$USER_INPUT" = "yes" ]; then + # Perform updates + + if [ "$BOT_NEEDS_UPDATE" = true ]; then + echo "Updating 'my-core-bot' image to: $NEW_BOT_VER" + yq eval ".services[\"my-core-bot\"].image = \"$NEW_BOT_VER\"" -i "$COMPOSE_FILE" + fi + + if [ "$VIS_NEEDS_UPDATE" = true ]; then + echo "Updating 'visualizer' image to: $NEW_VIS_VER" + yq eval ".services[\"visualizer\"].image = \"$NEW_VIS_VER\"" -i "$COMPOSE_FILE" + fi + + echo "" + echo "Updated docker-compose.yml" + echo -e "${RED}Please close the current devcontainer. Then, from your local terminal in the bot folder, run this command to update and restart:${NC}" + echo "make update && make devcontainer" + echo "" + echo "Note: Please also commit the changes of the docker-compose.yml file!" + echo "" + + exit 1 +else + exit 0 +fi diff --git a/bots/ts/softcore/scripts/check_update_configs.sh b/bots/ts/softcore/scripts/check_update_configs.sh new file mode 100755 index 00000000..b38c264c --- /dev/null +++ b/bots/ts/softcore/scripts/check_update_configs.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +check_config() { + local config_name="$1" + local local_path="configs/${config_name}.config.json" + local url="[[event_url]]/${config_name}-config" + + local remote_content + remote_content=$(curl -sf --max-time 1 "$url") || { + echo "Failed to fetch: $url - this might mean you are playing with an out of date config. You should address this." >&2 + return 1 + } + + local local_content + local_content=$(<"$local_path") + + if [[ "$remote_content" == "$local_content" ]]; then + return 0 + fi + + echo "" + echo "==================================================" + echo "โš  CONFIG CHANGED: ${config_name}.config.json" + echo "==================================================" + echo "" + + local backup_path="${local_path%.*}_old.${local_path##*.}" + + cp "$local_path" "$backup_path" + echo "$remote_content" > "$local_path" + + echo "The following changes have occurred:" + diff --color=auto -u "$backup_path" "$local_path" || true +} + +check_config "game" +check_config "server" From c83df090e1eb2d9f9b45cd6d690487a510e2bed5 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sat, 21 Feb 2026 18:24:26 +0100 Subject: [PATCH 02/15] add Dockerfile updates and initial configurations for "softcore" and "hardcore" TypeScript bots --- .github/workflows/my-core-bot-c-Dockerfile | 2 + .github/workflows/my-core-bot-ts-Dockerfile | 8 ++ bots/ts/hardcore/configs/game.config.json | 129 ++++++++++++++++++++ bots/ts/hardcore/configs/server.config.json | 14 +++ bots/ts/softcore/configs/game.config.json | 109 +++++++++++++++++ bots/ts/softcore/configs/server.config.json | 14 +++ 6 files changed, 276 insertions(+) create mode 100644 bots/ts/hardcore/configs/game.config.json create mode 100644 bots/ts/hardcore/configs/server.config.json create mode 100644 bots/ts/softcore/configs/game.config.json create mode 100644 bots/ts/softcore/configs/server.config.json diff --git a/.github/workflows/my-core-bot-c-Dockerfile b/.github/workflows/my-core-bot-c-Dockerfile index 70d4881f..97067c73 100644 --- a/.github/workflows/my-core-bot-c-Dockerfile +++ b/.github/workflows/my-core-bot-c-Dockerfile @@ -32,3 +32,5 @@ COPY --from=connection /connection/core_lib.a /core/con_lib.a # SHELL ["/bin/bash", "-c"] # ENV SHELL=/bin/bash + +CMD ["tail", "-f", "/dev/null"] diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile index d46d9c3a..6210b420 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -4,6 +4,12 @@ ARG SERVER_IMAGE="ghcr.io/42core-team/server" FROM oven/bun:latest AS bun-base WORKDIR /app +RUN apt-get update && \ + apt-get install -y make npm && \ + rm -rf /var/lib/apt/lists/* + +RUN bun add -g typescript + FROM ${SERVER_IMAGE}:${TAG_NAME} AS game # Build connection library from monorepo sources @@ -16,3 +22,5 @@ FROM bun-base AS release COPY --from=game /core/server /core/server COPY --from=game /core/data /core/data COPY --from=connection /connection/ /core/ts/client_lib/ + +CMD ["tail", "-f", "/dev/null"] diff --git a/bots/ts/hardcore/configs/game.config.json b/bots/ts/hardcore/configs/game.config.json new file mode 100644 index 00000000..87cc60f4 --- /dev/null +++ b/bots/ts/hardcore/configs/game.config.json @@ -0,0 +1,129 @@ +/* + For this event, there are 5 different unit types available: + 1. Warrior: A melee combat unit that excels in attacking enemy cores and units. + 2. Miner: A unit specialized in breaking hard objects such as gem deposits and walls. + 3. Carrier: A non-combat unit that is very quick, even when carrying many gems. + 4. Builder: A versatile unit that can construct walls to fortify positions. + 5. Bomberman: A unit capable of planting bombs to deal area damage to enemies and structures. +*/ +{ + "gridSize": 20, // tile count in each x and y directions of map, map will always be square + "seed": "", // if server is run twice with same seed, exact same world is generated. specify any string of any length, leave empty for a random seed + "idleIncome": 3, // amount of gems automatically deposited into each core each tick before the timeout + "idleIncomeTimeOut": 100, // how many ticks the idleIncome last before no more gems are deposited + "depositHp": 50, // default HP when a deposit object is spawned + "depositIncome": 100, // default amount of gems a deposit is spawned with, might be modified depending on world generator + "gemPileIncome": 35, // default amount of gems a gem pile is spawned with, might be modified depending on world generator + "coreHp": 99, // health points of a core when its spawned + "coreSpawnCooldown": 25, // cooldown in in ticks a core must wait after spawning a unit before it can spawn another unit + "initialBalance": 175, // default amount of gems a core is spawned with, might be modified depending on world generator + "wallHp": 77, // health points of a wall when its spawned + "wallBuildCost": 10, // amount of gems a wall builder unit must pay to build a wall + "bombHp": 10, // health points of a bomb when its spawned + "bombCountdown": 8, // countdown in ticks from when a bomb is first attacked to when it explodes + "bombThrowCost": 24, // amount of gems a bomb builder unit must pay to build a wall + "bombReach": 2, // bomb explosion range + "bombDamageCore": 49, // damage a bomb does to a core + "bombDamageUnit": 15, // damage a bomb does to a unit + "bombDamageDeposit": 25, // damage a bomb does to a deposit + "worldGenerator": "jigsaw", + "worldGeneratorConfig": { + "templatePlaceAttemptCount": 5000, + "additionalWallPlaceAttemptCount": 50, + "depositCount": 35, + "gemPileCount": 25, + "minTemplateSpacing": 1, + "minCoreDistance": 5, + "depositAdditionalIncomePerAdjacentWall": 40, + "depositMultiplierIfFullySurrounded": 1.3, + "randomDepositIncomeVariation": 100, + "randomGemPileIncomeVariation": 20 + }, + "units": [ + { + "name": "Warrior", // name, for aesthetic / display purposes only + "visualizer_asset_path": "warrior", // folder that the units visuals are picked from by visualizer + "cost": 150, // amount of gems needed to spawn the unit + "hp": 22, // amount of healthpoints the unit spawns with + "baseActionCooldown": 3, // base delay between action executions + "maxActionCooldown": 12, // max delay between action executions if unit is holding a lot of gems + "balancePerCooldownStep": 12, // defines increase of delay between action executions, see wiki for details + "damageCore": 7, // damage the unit does to a core + "damageUnit": 6, // damage the unit does to a unit + "damageDeposit": 2, // damage the unit does to a deposit + "damageWall": 2, // damage the unit does to a wall + "damageBomb": 4, // damage the unit does to a bomb + "buildType": "none" // whether & what the unit can build + }, + { + "name": "Miner", + "visualizer_asset_path": "miner", + "cost": 100, + "hp": 15, + "baseActionCooldown": 4, + "maxActionCooldown": 9, + "balancePerCooldownStep": 35, + "damageCore": 3, + "damageUnit": 2, + "damageDeposit": 10, + "damageWall": 15, + "damageBomb": 6, + "buildType": "none" + }, + { + "name": "Carrier", + "visualizer_asset_path": "carrier", + "cost": 175, + "hp": 5, + "baseActionCooldown": 0, + "maxActionCooldown": 1, + "balancePerCooldownStep": 999999, + "damageCore": 0, + "damageUnit": 0, + "damageDeposit": 0, + "damageWall": 1, + "damageBomb": 2, + "buildType": "none" + }, + { + "name": "Builder", + "visualizer_asset_path": "builder", + "cost": 200, + "hp": 13, + "baseActionCooldown": 6, + "maxActionCooldown": 8, + "balancePerCooldownStep": 10, + "damageCore": 3, + "damageUnit": 3, + "damageDeposit": 2, + "damageWall": 8, + "damageBomb": 5, + "buildType": "wall" + }, + { + "name": "Bomberman", + "visualizer_asset_path": "bomberman", + "cost": 400, + "hp": 8, + "baseActionCooldown": 4, + "maxActionCooldown": 4, + "balancePerCooldownStep": 99999, + "damageCore": 4, + "damageUnit": 3, + "damageDeposit": 5, + "damageWall": 3, + "damageBomb": 5, + "buildType": "bomb" + } + ], + "corePositions": [ + { + "x": 0, + "y": 0 + }, + { + "x": 19, + "y": 19 + } + ] +} \ No newline at end of file diff --git a/bots/ts/hardcore/configs/server.config.json b/bots/ts/hardcore/configs/server.config.json new file mode 100644 index 00000000..2c11aad3 --- /dev/null +++ b/bots/ts/hardcore/configs/server.config.json @@ -0,0 +1,14 @@ +{ + "replayFolderPaths": [ + "/workspace/replays", + "/workspaces/monorepo", + "/workspaces/monorepo/visualizer/public/replays", + "./replays" + ], + "timeoutTicks": 30000, + "timeoutMs": 3000000, + "clientWaitTimeoutMs": 50000, + "clientConnectTimeoutMs": 30000, + "clientPacketsMaxSizeKb": 16, + "enableTerminalVisualizer": false +} \ No newline at end of file diff --git a/bots/ts/softcore/configs/game.config.json b/bots/ts/softcore/configs/game.config.json new file mode 100644 index 00000000..350ed103 --- /dev/null +++ b/bots/ts/softcore/configs/game.config.json @@ -0,0 +1,109 @@ +/* + For this event, there are 4 different unit types available: + 1. Warrior: A melee combat unit that excels in attacking enemy cores and units. + 2. Miner: A unit specialized in breaking hard objects such as gem deposits and walls. + 3. Carrier: A non-combat unit that is very quick, even when carrying many gems. + 4. Tank: A heavily armored and slow unit that packs a huge punch. +*/ +{ + "gridSize": 20, // tile count in each x and y directions of map, map will always be square + "seed": "", // if server is run twice with same seed, exact same world is generated. specify any string of any length, leave empty for a random seed + "idleIncome": 2, // amount of gems automatically deposited into each core each tick before the timeout + "idleIncomeTimeOut": 1000, // how many ticks the idleIncome last before no more gems are deposited + "depositHp": 50, // default HP when a deposit object is spawned + "depositIncome": 150, // default amount of gems a deposit is spawned with, might be modified depending on world generator + "gemPileIncome": 35, // default amount of gems a gem pile is spawned with, might be modified depending on world generator + "coreHp": 200, // health points of a core when its spawned + "coreSpawnCooldown": 20, // cooldown in in ticks a core must wait after spawning a unit before it can spawn another unit + "initialBalance": 200, // default amount of gems a core is spawned with, might be modified depending on world generator + "wallHp": 50, // health points of a wall when its spawned + "wallBuildCost": 20, // amount of gems a wall builder unit must pay to build a wall + "bombHp": 25, // health points of a bomb when its spawned + "bombCountdown": 10, // countdown in ticks from when a bomb is first attacked to when it explodes + "bombThrowCost": 50, // amount of gems a bomb builder unit must pay to build a wall + "bombReach": 3, // bomb explosion range + "bombDamageCore": 50, // damage a bomb does to a core + "bombDamageUnit": 30, // damage a bomb does to a unit + "bombDamageDeposit": 40, // damage a bomb does to a deposit + "worldGenerator": "sparse", + "worldGeneratorConfig": { + "depositCount": 20, + "depositBalanceVariation": 100, + "gemPileCount": 10, + "gemPileBalanceVariation": 50, + "wallCount": 30, + "coreBuffer": 3 + }, + "units": [ + { + "name": "Warrior", // name, for aesthetic / display purposes only + "visualizer_asset_path": "warrior", // folder that the units visuals are picked from by visualizer + "cost": 150, // amount of gems needed to spawn the unit + "hp": 35, // amount of healthpoints the unit spawns with + "baseActionCooldown": 5, // base delay between action executions + "maxActionCooldown": 12, // max delay between action executions if unit is holding a lot of gems + "balancePerCooldownStep": 12, // defines increase of delay between action executions, see wiki for details + "damageCore": 12, // damage the unit does to a core + "damageUnit": 6, // damage the unit does to a unit + "damageDeposit": 4, // damage the unit does to a deposit + "damageWall": 5, // damage the unit does to a wall + "damageBomb": 5, // damage the unit does to a bomb + "buildType": "none" // whether & what the unit can build + }, + { + "name": "Miner", + "visualizer_asset_path": "miner", + "cost": 100, + "hp": 20, + "baseActionCooldown": 4, + "maxActionCooldown": 8, + "balancePerCooldownStep": 35, + "damageCore": 5, + "damageUnit": 2, + "damageDeposit": 10, + "damageWall": 12, + "damageBomb": 5, + "buildType": "none" + }, + { + "name": "Carrier", + "visualizer_asset_path": "carrier", + "cost": 200, + "hp": 10, + "baseActionCooldown": 0, + "maxActionCooldown": 1, + "balancePerCooldownStep": 99999, + "damageCore": 0, + "damageUnit": 0, + "damageDeposit": 0, + "damageWall": 2, + "damageBomb": 2, + "buildType": "none" + }, + { + "name": "Tank", + "visualizer_asset_path": "tank", + "cost": 500, + "hp": 75, + "baseActionCooldown": 12, + "maxActionCooldown": 16, + "balancePerCooldownStep": 10, + "damageCore": 50, + "damageUnit": 50, + "damageDeposit": 6, + "damageWall": 25, + "damageBomb": 25, + "buildType": "none" + } + ], + "corePositions": [ + { + "x": 0, + "y": 0 + }, + { + "x": 19, + "y": 19 + } + ] +} \ No newline at end of file diff --git a/bots/ts/softcore/configs/server.config.json b/bots/ts/softcore/configs/server.config.json new file mode 100644 index 00000000..2c11aad3 --- /dev/null +++ b/bots/ts/softcore/configs/server.config.json @@ -0,0 +1,14 @@ +{ + "replayFolderPaths": [ + "/workspace/replays", + "/workspaces/monorepo", + "/workspaces/monorepo/visualizer/public/replays", + "./replays" + ], + "timeoutTicks": 30000, + "timeoutMs": 3000000, + "clientWaitTimeoutMs": 50000, + "clientConnectTimeoutMs": 30000, + "clientPacketsMaxSizeKb": 16, + "enableTerminalVisualizer": false +} \ No newline at end of file From deaf7cc735d78cee6a207f50efd089882b87b0d3 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sat, 21 Feb 2026 18:24:58 +0100 Subject: [PATCH 03/15] no diff --- bots/ts/hardcore/scripts/check_update_configs.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bots/ts/hardcore/scripts/check_update_configs.sh diff --git a/bots/ts/hardcore/scripts/check_update_configs.sh b/bots/ts/hardcore/scripts/check_update_configs.sh old mode 100644 new mode 100755 From cf6f6bbb327c5d5cadb4a1c1f5f84cb3894e97a9 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sat, 21 Feb 2026 21:11:50 +0100 Subject: [PATCH 04/15] update Dockerfile path in devcontainer and refactor `ClientLib` game state handling --- bots/ts/client_lib/client_lib.ts | 23 ++++++++++--------- .../hardcore/.devcontainer/docker-compose.yml | 6 +++-- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/bots/ts/client_lib/client_lib.ts b/bots/ts/client_lib/client_lib.ts index ad6002c7..434e8d1a 100644 --- a/bots/ts/client_lib/client_lib.ts +++ b/bots/ts/client_lib/client_lib.ts @@ -2,9 +2,10 @@ import * as net from 'net'; import type { GameState, Action, Obj, Pos } from './types'; import { ActionType, UnitType, ObjType, ObjState } from './types'; +export let game: GameState | null = null; + export class ClientLib { private socket: net.Socket | null = null; - private game: GameState | null = null; private actions: Action[] = []; private debugData: any[] = []; private buffer: string = ''; @@ -22,7 +23,7 @@ export class ClientLib { this.socket.on('data', (data: Buffer) => { this.handleData(data); - if (this.game && resolve) { + if (game && resolve) { resolve(); (resolve as any) = null; } @@ -88,7 +89,7 @@ export class ClientLib { } else { // Game state update if (parsed.tick !== undefined) { - this.game.elapsed_ticks = parsed.tick; + game.elapsed_ticks = parsed.tick; } if (parsed.objects && Array.isArray(parsed.objects)) { @@ -105,7 +106,7 @@ export class ClientLib { } // Decrement cooldowns manually as C lib does - for (const obj of this.game.objects) { + for (const obj of game.objects) { if (obj.type === ObjType.UNIT) { if (obj.s_unit.action_cooldown > 0) obj.s_unit.action_cooldown--; } else if (obj.type === ObjType.CORE) { @@ -115,7 +116,7 @@ export class ClientLib { // After state update, we should have a tick if (this.tickCallback) { - this.tickCallback(this.game); + this.tickCallback(game); } // ALWAYS send actions back to the server to prevent timeouts this.sendActions(); @@ -126,19 +127,19 @@ export class ClientLib { } private applyDiff(diff: any): void { - if (!this.game) return; + if (!game) return; const id = diff.id; if (id === undefined) return; if (diff.state === 'dead') { - this.game.objects = this.game.objects.filter(o => o.id !== id); + game.objects = game.objects.filter(o => o.id !== id); return; } - let obj = this.game.objects.find(o => o.id === id); + let obj = game.objects.find(o => o.id === id); if (!obj) { - obj = { + obj = { id, state: ObjState.ALIVE, pos: { x: 0, y: 0 }, @@ -147,7 +148,7 @@ export class ClientLib { s_deposit_gems_pile: { gems: 0 }, s_bomb: { countdown: 0 } } as Obj; - this.game.objects.push(obj); + game.objects.push(obj); } if (diff.type !== undefined) obj.type = diff.type; @@ -264,6 +265,6 @@ export class ClientLib { } public getGame(): GameState | null { - return this.game; + return game; } } diff --git a/bots/ts/hardcore/.devcontainer/docker-compose.yml b/bots/ts/hardcore/.devcontainer/docker-compose.yml index d2310594..c427ec3a 100644 --- a/bots/ts/hardcore/.devcontainer/docker-compose.yml +++ b/bots/ts/hardcore/.devcontainer/docker-compose.yml @@ -1,7 +1,9 @@ services: my-core-bot: - image: ghcr.io/42core-team/my-core-bot-ts:dev - platform: linux/amd64 + build: + context: ../../../.. + dockerfile: .github/workflows/my-core-bot-ts-Dockerfile + volumes: - ..:/workspace - replays:/workspace/replays From 499e03e09bd9923eebd6e255713d216f6fcea70a Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Mon, 23 Feb 2026 23:21:32 +0100 Subject: [PATCH 05/15] refactor client lib --- .github/workflows/my-core-bot-ts-Dockerfile | 7 +- bots/ts/client_lib/client_lib.ts | 498 +++++++++++--------- bots/ts/hardcore/gridmaster/bun.lock | 18 + bots/ts/hardcore/gridmaster/index.ts | 35 +- bots/ts/hardcore/gridmaster/package.json | 5 +- bots/ts/hardcore/my-core-bot/bun.lock | 18 + bots/ts/hardcore/my-core-bot/index.ts | 32 +- bots/ts/hardcore/my-core-bot/package.json | 5 +- bots/ts/softcore/gridmaster/bun.lock | 18 + bots/ts/softcore/gridmaster/index.ts | 27 +- bots/ts/softcore/gridmaster/package.json | 5 +- bots/ts/softcore/my-core-bot/bun.lock | 18 + bots/ts/softcore/my-core-bot/index.ts | 26 +- bots/ts/softcore/my-core-bot/package.json | 5 +- 14 files changed, 400 insertions(+), 317 deletions(-) create mode 100644 bots/ts/hardcore/gridmaster/bun.lock create mode 100644 bots/ts/hardcore/my-core-bot/bun.lock create mode 100644 bots/ts/softcore/gridmaster/bun.lock create mode 100644 bots/ts/softcore/my-core-bot/bun.lock diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile index 6210b420..a404f436 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -12,15 +12,10 @@ RUN bun add -g typescript FROM ${SERVER_IMAGE}:${TAG_NAME} AS game -# Build connection library from monorepo sources -FROM bun-base AS connection -WORKDIR /connection -COPY bots/ts/client_lib/ ./ - FROM bun-base AS release COPY --from=game /core/server /core/server COPY --from=game /core/data /core/data -COPY --from=connection /connection/ /core/ts/client_lib/ +COPY bots/ts/client_lib/ /core/client_lib/ CMD ["tail", "-f", "/dev/null"] diff --git a/bots/ts/client_lib/client_lib.ts b/bots/ts/client_lib/client_lib.ts index 434e8d1a..457f278b 100644 --- a/bots/ts/client_lib/client_lib.ts +++ b/bots/ts/client_lib/client_lib.ts @@ -1,270 +1,308 @@ -import * as net from 'net'; -import type { GameState, Action, Obj, Pos } from './types'; -import { ActionType, UnitType, ObjType, ObjState } from './types'; +import * as net from 'node:net'; +import type {GameState, Action, Obj, Pos} from './types'; +import {ActionType, UnitType, ObjType, ObjState} from './types'; export let game: GameState | null = null; -export class ClientLib { - private socket: net.Socket | null = null; - private actions: Action[] = []; - private debugData: any[] = []; - private buffer: string = ''; - - constructor(private teamName: string, private teamId: number, private debug: boolean = false) {} - - public async connect(host: string = '127.0.0.1', port: number = 4444): Promise { - return new Promise((resolve, reject) => { - this.socket = new net.Socket(); - - this.socket.connect(port, host, () => { - if (this.debug) console.log(`Connected to ${host}:${port}`); - this.sendLogin(); - }); - - this.socket.on('data', (data: Buffer) => { - this.handleData(data); - if (game && resolve) { - resolve(); - (resolve as any) = null; - } - }); - - this.socket.on('error', (err) => { - console.error('Socket error:', err); - reject(err); - }); - - this.socket.on('close', () => { - if (this.debug) console.log('Connection closed'); - process.exit(0); - }); - }); +// Internal module state replacing the class fields +let socket: net.Socket | null = null; +let actions: Action[] = []; +let debugData: any[] = []; +let buffer = ''; +let host = process.env.SERVER_IP || '127.0.0.1'; +let port = process.env.SERVER_PORT ? Number.parseInt(process.env.SERVER_PORT) : 4444; +let teamId: number = process.argv.length >= 3 ? Number(process.argv[2]) : 0; +let teamNameInternal = ''; +let debug = false; +let tickCallback: (() => void) | null = null; + +function validateTeamId(): void { + if (Number.isNaN(teamId)) { + console.error('Invalid team id. Usage: ./bot '); + process.exit(1); } +} - private sendLogin(): void { - const loginMsg = { - password: "42", - id: this.teamId, - name: this.teamName - }; - this.sendJson(loginMsg); - } +async function connectInternal(): Promise { + return new Promise((resolve, reject) => { + socket = new net.Socket(); - private sendJson(obj: any): void { - if (this.socket) { - const str = JSON.stringify(obj) + '\n'; - this.socket.write(str); - if (this.debug) console.log('Sent:', str); - } - } + socket.connect(port, host, () => { + if (debug) console.log(`Connected to ${host}:${port}`); + sendLogin(); + }); - private handleData(data: Buffer): void { - this.buffer += data.toString(); - let newlineIndex; - while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) { - const line = this.buffer.slice(0, newlineIndex); - this.buffer = this.buffer.slice(newlineIndex + 1); - if (line.trim()) { - this.handleLine(line); + socket.on('data', (data: Buffer) => { + handleData(data); + if (game && resolve) { + resolve(); + (resolve as any) = null; } - } + }); + + socket.on('error', (err) => { + console.error('Socket error:', err); + reject(err); + }); + + socket.on('close', () => { + if (debug) console.log('Connection closed'); + process.exit(0); + }); + }); +} + +function sendLogin(): void { + const loginMsg = { + password: '42', + id: teamId, + name: teamNameInternal + }; + sendJson(loginMsg); +} + +function sendJson(obj: any): void { + if (socket) { + const str = JSON.stringify(obj) + '\n'; + socket.write(str); + if (debug) console.log('Sent:', str); } +} - private handleLine(line: string): void { - try { - const parsed = JSON.parse(line); - if (this.debug) console.log('Received:', line); - - if (!this.game) { - // Initial config - this.game = { - elapsed_ticks: 0, - config: parsed, - my_team_id: this.teamId, - objects: [] - }; - if (this.debug) console.log('Config received'); - // Send first packet of actions right after receiving config to start game loop - this.sendActions(); - } else { - // Game state update - if (parsed.tick !== undefined) { - game.elapsed_ticks = parsed.tick; - } - - if (parsed.objects && Array.isArray(parsed.objects)) { - for (const diff of parsed.objects) { - this.applyDiff(diff); - } - } - - // print errors from last tick if there were any - if (parsed.errors && Array.isArray(parsed.errors)) { - for (const error of parsed.errors) { - console.error(`\x1b[31m${error}\x1b[0m`); - } - } - - // Decrement cooldowns manually as C lib does - for (const obj of game.objects) { - if (obj.type === ObjType.UNIT) { - if (obj.s_unit.action_cooldown > 0) obj.s_unit.action_cooldown--; - } else if (obj.type === ObjType.CORE) { - if (obj.s_core.spawn_cooldown > 0) obj.s_core.spawn_cooldown--; - } - } - - // After state update, we should have a tick - if (this.tickCallback) { - this.tickCallback(game); - } - // ALWAYS send actions back to the server to prevent timeouts - this.sendActions(); - } - } catch (e) { - console.error('Error parsing JSON:', e, 'Line:', line); +function handleData(data: Buffer): void { + buffer += data.toString(); + let newlineIndex; + while ((newlineIndex = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + if (line.trim()) { + handleLine(line); } } +} - private applyDiff(diff: any): void { - if (!game) return; - - const id = diff.id; - if (id === undefined) return; - - if (diff.state === 'dead') { - game.objects = game.objects.filter(o => o.id !== id); +function handleLine(line: string): void { + try { + const parsed = JSON.parse(line); + if (debug) console.log('Received:', line); + + if (!game) { + // Initial config + game = { + elapsed_ticks: 0, + config: parsed, + my_team_id: teamId, + objects: [] + }; + if (debug) console.log('Config received'); + // Send first packet of actions right after receiving config to start game loop + sendActions(); return; } - let obj = game.objects.find(o => o.id === id); - if (!obj) { - obj = { - id, - state: ObjState.ALIVE, - pos: { x: 0, y: 0 }, - s_core: { team_id: 0, gems: 0, spawn_cooldown: 0, balance: 0 }, - s_unit: { unit_type: 0, team_id: 0, gems: 0, action_cooldown: 0, balance: 0 }, - s_deposit_gems_pile: { gems: 0 }, - s_bomb: { countdown: 0 } - } as Obj; - game.objects.push(obj); - } + // Update game state from server message + updateGameState(parsed); - if (diff.type !== undefined) obj.type = diff.type; - if (diff.x !== undefined || diff.y !== undefined) { - if (diff.x !== undefined) obj.pos.x = diff.x; - if (diff.y !== undefined) obj.pos.y = diff.y; - } - if (diff.hp !== undefined) obj.hp = diff.hp; - - if (diff.teamId !== undefined) { - obj.s_core.team_id = diff.teamId; - obj.s_unit.team_id = diff.teamId; - } - if (diff.gems !== undefined) { - obj.s_core.gems = diff.gems; - obj.s_core.balance = diff.gems; - obj.s_unit.gems = diff.gems; - obj.s_unit.balance = diff.gems; - obj.s_deposit_gems_pile.gems = diff.gems; - } - if (diff.ActionCooldown !== undefined) obj.s_unit.action_cooldown = diff.ActionCooldown; - if (diff.SpawnCooldown !== undefined) obj.s_core.spawn_cooldown = diff.SpawnCooldown; - if (diff.unit_type !== undefined) obj.s_unit.unit_type = diff.unit_type; - if (diff.countdown !== undefined) obj.s_bomb.countdown = diff.countdown; - } + // Print errors from last tick if there were any + printServerErrors(parsed?.errors); - private tickCallback: ((game: GameState) => void) | null = null; + // Decrement cooldowns manually as C lib does + decrementCooldowns(); - public startGame(callback: (game: GameState) => void): void { - this.tickCallback = callback; - } + // After state update, we should have a tick + if (tickCallback) + tickCallback(); - private sendActions(): void { - const packet = { - actions: this.actions, - debug_data: this.debugData - }; - this.sendJson(packet); - this.actions = []; - this.debugData = []; + // ALWAYS send actions back to the server to prevent timeouts + sendActions(); + } catch (e) { + console.error('Error parsing JSON:', e, 'Line:', line); } +} - public createUnit(unitType: UnitType): void { - this.actions.push({ - type: ActionType.CREATE, - unit_type: unitType - }); +function updateGameState(parsed: any): void { + if (!game || !parsed) return; + if (parsed.tick !== undefined) { + game.elapsed_ticks = parsed.tick; } + if (parsed.objects && Array.isArray(parsed.objects)) { + for (const diff of parsed.objects) { + applyDiff(diff); + } + } +} - public move(unit: Obj, pos: Pos): void { - this.actions.push({ - type: ActionType.MOVE, - unit_id: unit.id, - x: pos.x, - y: pos.y - }); +function printServerErrors(errors: any[]): void { + if (!errors || !Array.isArray(errors)) return; + for (const error of errors) { + console.error(`\x1b[31m${error}\x1b[0m`); } +} - public attack(attacker: Obj, target: Obj): void { - this.actions.push({ - type: ActionType.ATTACK, - unit_id: attacker.id, - target_id: target.id - }); +function decrementCooldowns(): void { + if (!game) return; + for (const obj of game.objects) { + if (obj.type === ObjType.UNIT) { + if (obj.s_unit.action_cooldown > 0) obj.s_unit.action_cooldown--; + } else if (obj.type === ObjType.CORE) { + if (obj.s_core.spawn_cooldown > 0) obj.s_core.spawn_cooldown--; + } } +} - public transferGems(source: Obj, targetPos: Pos, amount: number): void { - this.actions.push({ - type: ActionType.TRANSFER, - source_id: source.id, - x: targetPos.x, - y: targetPos.y, - amount: amount - }); +function applyDiff(diff: any): void { + if (!game) return; + + const id = diff.id; + if (id === undefined) return; + + if (diff.state === 'dead') { + game.objects = game.objects.filter(o => o.id !== id); + return; } - public build(builder: Obj, pos: Pos): void { - this.actions.push({ - type: ActionType.BUILD, - unit_id: builder.id, - x: pos.x, - y: pos.y - }); + const obj = findOrInitializeObject(id); + updateObjectProperties(obj, diff); + updateTypeSpecificProperties(obj, diff); +} + +function findOrInitializeObject(id: number): Obj { + let obj = game!.objects.find(o => o.id === id); + if (!obj) { + obj = { + id, + state: ObjState.ALIVE, + pos: {x: 0, y: 0}, + s_core: {team_id: 0, gems: 0, spawn_cooldown: 0, balance: 0}, + s_unit: {unit_type: 0, team_id: 0, gems: 0, action_cooldown: 0, balance: 0}, + s_deposit_gems_pile: {gems: 0}, + s_bomb: {countdown: 0} + } as Obj; + game!.objects.push(obj); } + return obj; +} - public addDebugData(data: any): void { - this.debugData.push(data); +function updateObjectProperties(obj: Obj, diff: any): void { + if (diff.type !== undefined) obj.type = diff.type; + if (diff.x !== undefined || diff.y !== undefined) { + if (diff.x !== undefined) obj.pos.x = diff.x; + if (diff.y !== undefined) obj.pos.y = diff.y; } + if (diff.hp !== undefined) obj.hp = diff.hp; +} - public core_debug_addObjectInfo(obj: Obj, info: string): void { - let entry = this.debugData.find(d => d.object_id === obj.id); - if (!entry) { - entry = { - object_id: obj.id, - object_info: "", - object_path: [] - }; - this.debugData.push(entry); - } - entry.object_info += info; +function updateTypeSpecificProperties(obj: Obj, diff: any): void { + if (diff.teamId !== undefined) { + obj.s_core.team_id = diff.teamId; + obj.s_unit.team_id = diff.teamId; } + if (diff.gems !== undefined) { + obj.s_core.gems = diff.gems; + obj.s_core.balance = diff.gems; + obj.s_unit.gems = diff.gems; + obj.s_unit.balance = diff.gems; + obj.s_deposit_gems_pile.gems = diff.gems; + } + if (diff.ActionCooldown !== undefined) obj.s_unit.action_cooldown = diff.ActionCooldown; + if (diff.SpawnCooldown !== undefined) obj.s_core.spawn_cooldown = diff.SpawnCooldown; + if (diff.unit_type !== undefined) obj.s_unit.unit_type = diff.unit_type; + if (diff.countdown !== undefined) obj.s_bomb.countdown = diff.countdown; +} - public core_debug_addObjectPathStep(unit: Obj, pos: Pos): void { - let entry = this.debugData.find(d => d.object_id === unit.id); - if (!entry) { - entry = { - object_id: unit.id, - object_info: "", - object_path: [] - }; - this.debugData.push(entry); - } - entry.object_path.push({ x: pos.x, y: pos.y }); +function sendActions(): void { + const packet = { + actions: actions, + debug_data: debugData + }; + sendJson(packet); + actions = []; + debugData = []; +} + +// Exported API: previously public methods of the class +export function coreCreateUnit(unitType: UnitType): void { + actions.push({ + type: ActionType.CREATE, + unit_type: unitType + }); +} + +export function coreActionMove(unit: Obj, pos: Pos): void { + actions.push({ + type: ActionType.MOVE, + unit_id: unit.id, + x: pos.x, + y: pos.y + }); +} + +export function coreActionAttack(attacker: Obj, target: Obj): void { + actions.push({ + type: ActionType.ATTACK, + unit_id: attacker.id, + target_id: target.id + }); +} + +export function coreActionTransferGems(source: Obj, targetPos: Pos, amount: number): void { + actions.push({ + type: ActionType.TRANSFER, + source_id: source.id, + x: targetPos.x, + y: targetPos.y, + amount: amount + }); +} + +export function coreActionBuild(builder: Obj, pos: Pos): void { + actions.push({ + type: ActionType.BUILD, + unit_id: builder.id, + x: pos.x, + y: pos.y + }); +} + +export function coreDebugAddObjectInfo(obj: Obj, info: string): void { + let entry = debugData.find(d => d.object_id === obj.id); + if (!entry) { + entry = { + object_id: obj.id, + object_info: '', + object_path: [] + }; + debugData.push(entry); } + entry.object_info += info; +} - public getGame(): GameState | null { - return game; +export function coreDebugAddObjectPathStep(unit: Obj, pos: Pos): void { + let entry = debugData.find(d => d.object_id === unit.id); + if (!entry) { + entry = { + object_id: unit.id, + object_info: '', + object_path: [] + }; + debugData.push(entry); } + entry.object_path.push({x: pos.x, y: pos.y}); +} + +// New entry point replacing the class: set callback and connect +export async function startGame(teamName: string, onTick: () => void): Promise { + teamNameInternal = teamName; + tickCallback = onTick; + + // Re-evaluate env/argv on each start, in case caller changed them + host = process.env.SERVER_IP || '127.0.0.1'; + port = process.env.SERVER_PORT ? Number.parseInt(process.env.SERVER_PORT) : 4444; + teamId = process.argv.length >= 3 ? Number(process.argv[2]) : 0; + validateTeamId(); + + // Enable verbose logs via env var if desired + debug = process.env.CLIENT_LIB_DEBUG === '1' || process.env.CLIENT_LIB_DEBUG === 'true'; + + await connectInternal(); } diff --git a/bots/ts/hardcore/gridmaster/bun.lock b/bots/ts/hardcore/gridmaster/bun.lock new file mode 100644 index 00000000..e69d99ec --- /dev/null +++ b/bots/ts/hardcore/gridmaster/bun.lock @@ -0,0 +1,18 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "hardcore-gridmaster", + "dependencies": { + "@core/client-lib": "file:/core/client_lib/", + }, + }, + }, + "packages": { + "@core/client-lib": ["@core/client-lib@file:../../client_lib", { "dependencies": { "@types/node": "^20.11.0" } }], + + "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/bots/ts/hardcore/gridmaster/index.ts b/bots/ts/hardcore/gridmaster/index.ts index a46ac4a0..ee40e978 100644 --- a/bots/ts/hardcore/gridmaster/index.ts +++ b/bots/ts/hardcore/gridmaster/index.ts @@ -1,33 +1,22 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType} from '@core/client-lib/types'; +import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; -const teamId = process.argv.length >= 3 ? Number(process.argv[2]) : 0; -if (isNaN(teamId)) { - console.error("Invalid team id. Usage: ./bot "); - process.exit(1); -} - -const host = process.env.SERVER_IP || '127.0.0.1'; -const port = process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT) : 4444; -const bot = new ClientLib("Hardcore Gridmaster TS", teamId, true); - -bot.startGame((game: GameState) => { +function onTick() { // Create a warrior - bot.createUnit(UnitType.WARRIOR); + coreCreateUnit(UnitType.WARRIOR); - // Find opponent core - const opponentCore = game.objects.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + // Hardcore bot logic + const opponentCore = game?.objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); if (opponentCore) { - // Move all own units to opponent core - game.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { - bot.move(obj, opponentCore.pos); + game?.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + coreActionMove(obj, opponentCore.pos); } }); } -}); +} -bot.connect(); +startGame("Gridmaster TS Core Bot", onTick) diff --git a/bots/ts/hardcore/gridmaster/package.json b/bots/ts/hardcore/gridmaster/package.json index 1d5f8e98..37d1b619 100644 --- a/bots/ts/hardcore/gridmaster/package.json +++ b/bots/ts/hardcore/gridmaster/package.json @@ -4,9 +4,10 @@ "description": "Hardcore Gridmaster TS bot", "main": "index.ts", "scripts": { - "start": "bun run index.ts" + "start": "bun run index.ts", + "build": "bun build index.ts --target=bun --outfile dist/index.js" }, "dependencies": { - "@core/client-lib": "file:../../../core/ts/client_lib" + "@core/client-lib": "file:/core/client_lib/" } } diff --git a/bots/ts/hardcore/my-core-bot/bun.lock b/bots/ts/hardcore/my-core-bot/bun.lock new file mode 100644 index 00000000..ba40a5ea --- /dev/null +++ b/bots/ts/hardcore/my-core-bot/bun.lock @@ -0,0 +1,18 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "hardcore-my-core-bot", + "dependencies": { + "@core/client-lib": "file:/core/client_lib/", + }, + }, + }, + "packages": { + "@core/client-lib": ["@core/client-lib@file:../../client_lib", { "dependencies": { "@types/node": "^20.11.0" } }], + + "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/bots/ts/hardcore/my-core-bot/index.ts b/bots/ts/hardcore/my-core-bot/index.ts index b55a1544..31589556 100644 --- a/bots/ts/hardcore/my-core-bot/index.ts +++ b/bots/ts/hardcore/my-core-bot/index.ts @@ -1,32 +1,22 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType} from '@core/client-lib/types'; +import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; -const teamId = process.argv.length >= 3 ? Number(process.argv[2]) : 0; -if (isNaN(teamId)) { - console.error("Invalid team id. Usage: ./bot "); - process.exit(1); -} - -const host = process.env.SERVER_IP || '127.0.0.1'; -const port = process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT) : 4444; -const bot = new ClientLib("Hardcore TS Core Bot", teamId, true); - -bot.startGame((game: GameState) => { +function onTick() { // Create a warrior - bot.createUnit(UnitType.WARRIOR); + coreCreateUnit(UnitType.WARRIOR); // Hardcore bot logic - const opponentCore = game.objects.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + const opponentCore = game?.objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); if (opponentCore) { - game.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { - bot.move(obj, opponentCore.pos); + game?.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + coreActionMove(obj, opponentCore.pos); } }); } -}); +} -bot.connect(); +startGame("Hardcore TS Core Bot", onTick) diff --git a/bots/ts/hardcore/my-core-bot/package.json b/bots/ts/hardcore/my-core-bot/package.json index e1531185..309021cf 100644 --- a/bots/ts/hardcore/my-core-bot/package.json +++ b/bots/ts/hardcore/my-core-bot/package.json @@ -4,9 +4,10 @@ "description": "Hardcore TS Core Bot", "main": "index.ts", "scripts": { - "start": "bun run index.ts" + "start": "bun run index.ts", + "build": "bun build index.ts --target=bun --outfile dist/index.js" }, "dependencies": { - "@core/client-lib": "file:../../../core/ts/client_lib" + "@core/client-lib": "file:/core/client_lib/" } } diff --git a/bots/ts/softcore/gridmaster/bun.lock b/bots/ts/softcore/gridmaster/bun.lock new file mode 100644 index 00000000..5f6e6e08 --- /dev/null +++ b/bots/ts/softcore/gridmaster/bun.lock @@ -0,0 +1,18 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "softcore-gridmaster", + "dependencies": { + "@core/client-lib": "file:/core/client_lib/", + }, + }, + }, + "packages": { + "@core/client-lib": ["@core/client-lib@file:../../client_lib", { "dependencies": { "@types/node": "^20.11.0" } }], + + "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/bots/ts/softcore/gridmaster/index.ts b/bots/ts/softcore/gridmaster/index.ts index 9a0db800..ee40e978 100644 --- a/bots/ts/softcore/gridmaster/index.ts +++ b/bots/ts/softcore/gridmaster/index.ts @@ -1,25 +1,22 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType} from '@core/client-lib/types'; +import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; -const bot = new ClientLib("Gridmaster TS", 1); - -bot.startGame((game: GameState) => { +function onTick() { // Create a warrior - bot.createUnit(UnitType.WARRIOR); + coreCreateUnit(UnitType.WARRIOR); - // Find opponent core - const opponentCore = game.objects.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + // Hardcore bot logic + const opponentCore = game?.objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); if (opponentCore) { - // Move all own units to opponent core - game.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { - bot.move(obj, opponentCore.pos); + game?.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + coreActionMove(obj, opponentCore.pos); } }); } -}); +} -bot.connect(); +startGame("Gridmaster TS Core Bot", onTick) diff --git a/bots/ts/softcore/gridmaster/package.json b/bots/ts/softcore/gridmaster/package.json index 55da2b8a..36b54a21 100644 --- a/bots/ts/softcore/gridmaster/package.json +++ b/bots/ts/softcore/gridmaster/package.json @@ -4,9 +4,10 @@ "description": "Softcore Gridmaster TS bot", "main": "index.ts", "scripts": { - "start": "bun run index.ts" + "start": "bun run index.ts", + "build": "bun build index.ts --target=bun --outfile dist/index.js" }, "dependencies": { - "@core/client-lib": "file:../../../core/ts/client_lib" + "@core/client-lib": "file:/core/client_lib/" } } diff --git a/bots/ts/softcore/my-core-bot/bun.lock b/bots/ts/softcore/my-core-bot/bun.lock new file mode 100644 index 00000000..061d9bb0 --- /dev/null +++ b/bots/ts/softcore/my-core-bot/bun.lock @@ -0,0 +1,18 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "softcore-my-core-bot", + "dependencies": { + "@core/client-lib": "file:/core/client_lib/", + }, + }, + }, + "packages": { + "@core/client-lib": ["@core/client-lib@file:../../client_lib", { "dependencies": { "@types/node": "^20.11.0" } }], + + "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/bots/ts/softcore/my-core-bot/index.ts b/bots/ts/softcore/my-core-bot/index.ts index 16653cc1..31589556 100644 --- a/bots/ts/softcore/my-core-bot/index.ts +++ b/bots/ts/softcore/my-core-bot/index.ts @@ -1,24 +1,22 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType} from '@core/client-lib/types'; +import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; -const bot = new ClientLib("TS Core Bot", 1); - -bot.startGame((game: GameState) => { +function onTick() { // Create a warrior - bot.createUnit(UnitType.WARRIOR); + coreCreateUnit(UnitType.WARRIOR); - // Simple bot logic - const opponentCore = game.objects.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id + // Hardcore bot logic + const opponentCore = game?.objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); if (opponentCore) { - game.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game.my_team_id) { - bot.move(obj, opponentCore.pos); + game?.objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + coreActionMove(obj, opponentCore.pos); } }); } -}); +} -bot.connect(); +startGame("Hardcore TS Core Bot", onTick) diff --git a/bots/ts/softcore/my-core-bot/package.json b/bots/ts/softcore/my-core-bot/package.json index 4470241a..d8ea18ff 100644 --- a/bots/ts/softcore/my-core-bot/package.json +++ b/bots/ts/softcore/my-core-bot/package.json @@ -4,9 +4,10 @@ "description": "Softcore TS Core Bot", "main": "index.ts", "scripts": { - "start": "bun run index.ts" + "start": "bun run index.ts", + "build": "bun build index.ts --target=bun --outfile dist/index.js" }, "dependencies": { - "@core/client-lib": "file:../../../core/ts/client_lib" + "@core/client-lib": "file:/core/client_lib/" } } From 1b722daf09c2ebaa4213e2eb09f2fc2d080827a6 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Mon, 23 Feb 2026 23:34:14 +0100 Subject: [PATCH 06/15] update softcore bot: rename bot and clean up logic --- bots/ts/softcore/my-core-bot/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bots/ts/softcore/my-core-bot/index.ts b/bots/ts/softcore/my-core-bot/index.ts index 31589556..2ea4cfff 100644 --- a/bots/ts/softcore/my-core-bot/index.ts +++ b/bots/ts/softcore/my-core-bot/index.ts @@ -2,10 +2,8 @@ import {UnitType, ObjType} from '@core/client-lib/types'; import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { - // Create a warrior coreCreateUnit(UnitType.WARRIOR); - // Hardcore bot logic const opponentCore = game?.objects?.find(obj => obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); @@ -19,4 +17,4 @@ function onTick() { } } -startGame("Hardcore TS Core Bot", onTick) +startGame("Softcore TS Core Bot", onTick) From b07ce1cf0e593370afc67fd416c7aac425d278dd Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 14:22:00 +0100 Subject: [PATCH 07/15] clean code and update @types/node --- bots/ts/client_lib/package.json | 2 +- bots/ts/hardcore/gridmaster/index.ts | 2 -- bots/ts/hardcore/my-core-bot/index.ts | 2 -- bots/ts/softcore/gridmaster/index.ts | 2 -- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/bots/ts/client_lib/package.json b/bots/ts/client_lib/package.json index 8de25363..061e2207 100644 --- a/bots/ts/client_lib/package.json +++ b/bots/ts/client_lib/package.json @@ -5,6 +5,6 @@ "main": "client_lib.ts", "types": "types.ts", "dependencies": { - "@types/node": "^20.11.0" + "@types/node": "^25.5.0" } } diff --git a/bots/ts/hardcore/gridmaster/index.ts b/bots/ts/hardcore/gridmaster/index.ts index ee40e978..8fb30224 100644 --- a/bots/ts/hardcore/gridmaster/index.ts +++ b/bots/ts/hardcore/gridmaster/index.ts @@ -2,10 +2,8 @@ import {UnitType, ObjType} from '@core/client-lib/types'; import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { - // Create a warrior coreCreateUnit(UnitType.WARRIOR); - // Hardcore bot logic const opponentCore = game?.objects?.find(obj => obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); diff --git a/bots/ts/hardcore/my-core-bot/index.ts b/bots/ts/hardcore/my-core-bot/index.ts index 31589556..e1e68b28 100644 --- a/bots/ts/hardcore/my-core-bot/index.ts +++ b/bots/ts/hardcore/my-core-bot/index.ts @@ -2,10 +2,8 @@ import {UnitType, ObjType} from '@core/client-lib/types'; import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { - // Create a warrior coreCreateUnit(UnitType.WARRIOR); - // Hardcore bot logic const opponentCore = game?.objects?.find(obj => obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); diff --git a/bots/ts/softcore/gridmaster/index.ts b/bots/ts/softcore/gridmaster/index.ts index ee40e978..8fb30224 100644 --- a/bots/ts/softcore/gridmaster/index.ts +++ b/bots/ts/softcore/gridmaster/index.ts @@ -2,10 +2,8 @@ import {UnitType, ObjType} from '@core/client-lib/types'; import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { - // Create a warrior coreCreateUnit(UnitType.WARRIOR); - // Hardcore bot logic const opponentCore = game?.objects?.find(obj => obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id ); From 447b0dfa1d392b0387b1a0b63eadcc52048a41b5 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 14:25:24 +0100 Subject: [PATCH 08/15] use debian version of bun --- .github/workflows/my-core-bot-ts-Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile index a404f436..b8796af3 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -1,7 +1,7 @@ ARG TAG_NAME="dev" ARG SERVER_IMAGE="ghcr.io/42core-team/server" -FROM oven/bun:latest AS bun-base +FROM oven/bun:debian AS bun-base WORKDIR /app RUN apt-get update && \ From 87e535c743ebf81fc295dc73cb86025f4344ff0a Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 14:29:40 +0100 Subject: [PATCH 09/15] remove unused npm --- .github/workflows/my-core-bot-ts-Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile index b8796af3..7c5527e3 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -5,7 +5,7 @@ FROM oven/bun:debian AS bun-base WORKDIR /app RUN apt-get update && \ - apt-get install -y make npm && \ + apt-get install -y make && \ rm -rf /var/lib/apt/lists/* RUN bun add -g typescript From 177cfa1aaa240807a1501ce348671b5971952e38 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 14:30:20 +0100 Subject: [PATCH 10/15] remove unused CMD --- .github/workflows/my-core-bot-ts-Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile index 7c5527e3..78546b18 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -16,6 +16,4 @@ FROM bun-base AS release COPY --from=game /core/server /core/server COPY --from=game /core/data /core/data -COPY bots/ts/client_lib/ /core/client_lib/ - -CMD ["tail", "-f", "/dev/null"] +COPY bots/ts/client_lib/ /core/client_lib/ \ No newline at end of file From 61e8e4555a42080e50325a44e9754d9ad7d1e5fb Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 14:31:33 +0100 Subject: [PATCH 11/15] handle errors in tick callback execution --- bots/ts/client_lib/client_lib.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bots/ts/client_lib/client_lib.ts b/bots/ts/client_lib/client_lib.ts index 457f278b..6563845e 100644 --- a/bots/ts/client_lib/client_lib.ts +++ b/bots/ts/client_lib/client_lib.ts @@ -111,7 +111,11 @@ function handleLine(line: string): void { // After state update, we should have a tick if (tickCallback) - tickCallback(); + try { + tickCallback(); + } catch (e) { + console.error('Error executing tick callback:', e); + } // ALWAYS send actions back to the server to prevent timeouts sendActions(); From db76e6d33123387244b86378803c091b27d52e23 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 15:00:04 +0100 Subject: [PATCH 12/15] refactor client library: centralize game state access and add Dijkstra-based pathfinding --- bots/ts/client_lib/client_lib.ts | 92 ++++++++++++++-- bots/ts/client_lib/pathfinding.ts | 148 ++++++++++++++++++++++++++ bots/ts/hardcore/gridmaster/index.ts | 10 +- bots/ts/hardcore/my-core-bot/index.ts | 10 +- bots/ts/softcore/gridmaster/index.ts | 10 +- bots/ts/softcore/my-core-bot/index.ts | 10 +- 6 files changed, 253 insertions(+), 27 deletions(-) create mode 100644 bots/ts/client_lib/pathfinding.ts diff --git a/bots/ts/client_lib/client_lib.ts b/bots/ts/client_lib/client_lib.ts index 6563845e..ed9e02de 100644 --- a/bots/ts/client_lib/client_lib.ts +++ b/bots/ts/client_lib/client_lib.ts @@ -2,7 +2,8 @@ import * as net from 'node:net'; import type {GameState, Action, Obj, Pos} from './types'; import {ActionType, UnitType, ObjType, ObjState} from './types'; -export let game: GameState | null = null; +let game: GameState | null = null; +let isGameInitialized: boolean = false; // Internal module state replacing the class fields let socket: net.Socket | null = null; @@ -16,6 +17,12 @@ let teamNameInternal = ''; let debug = false; let tickCallback: (() => void) | null = null; +export function getGame(): GameState { + if (!isGameInitialized || !game) + throw new Error('Game is not initialized. Call initGame() before accessing game state.'); + return game; +} + function validateTeamId(): void { if (Number.isNaN(teamId)) { console.error('Invalid team id. Usage: ./bot '); @@ -94,6 +101,7 @@ function handleLine(line: string): void { my_team_id: teamId, objects: [] }; + isGameInitialized = true; if (debug) console.log('Config received'); // Send first packet of actions right after receiving config to start game loop sendActions(); @@ -125,7 +133,8 @@ function handleLine(line: string): void { } function updateGameState(parsed: any): void { - if (!game || !parsed) return; + if (!parsed) return; + const game = getGame(); if (parsed.tick !== undefined) { game.elapsed_ticks = parsed.tick; } @@ -144,7 +153,7 @@ function printServerErrors(errors: any[]): void { } function decrementCooldowns(): void { - if (!game) return; + const game = getGame(); for (const obj of game.objects) { if (obj.type === ObjType.UNIT) { if (obj.s_unit.action_cooldown > 0) obj.s_unit.action_cooldown--; @@ -155,11 +164,10 @@ function decrementCooldowns(): void { } function applyDiff(diff: any): void { - if (!game) return; - const id = diff.id; if (id === undefined) return; + const game = getGame(); if (diff.state === 'dead') { game.objects = game.objects.filter(o => o.id !== id); return; @@ -171,7 +179,7 @@ function applyDiff(diff: any): void { } function findOrInitializeObject(id: number): Obj { - let obj = game!.objects.find(o => o.id === id); + let obj = getGame().objects.find(o => o.id === id); if (!obj) { obj = { id, @@ -182,7 +190,7 @@ function findOrInitializeObject(id: number): Obj { s_deposit_gems_pile: {gems: 0}, s_bomb: {countdown: 0} } as Obj; - game!.objects.push(obj); + getGame().objects.push(obj); } return obj; } @@ -259,6 +267,76 @@ export function coreActionTransferGems(source: Obj, targetPos: Pos, amount: numb }); } +export function coreGetObjFromPos(pos: Pos): Obj | null { + return getGame().objects.find(o => o.pos.x === pos.x && o.pos.y === pos.y) || null; +} + +export function coreInternalIsPosValid(pos: Pos): boolean { + const game = getGame(); + return pos.x >= 0 && pos.y >= 0 && pos.x < game.config.gridSize && pos.y < game.config.gridSize; +} + +function coreStaticIsFriendlyObj(o: Obj | null): boolean { + if (!o) return false; + const game = getGame(); + if (o.type === ObjType.UNIT) return o.s_unit.team_id === game.my_team_id; + if (o.type === ObjType.CORE) return o.s_core.team_id === game.my_team_id; + return false; +} + +export function coreActionPathfind(unit: Obj, pos: Pos): void { + if (!unit || unit.type !== ObjType.UNIT) return; + if (unit.pos.x === pos.x && unit.pos.y === pos.y) return; + if (unit.s_unit.action_cooldown !== 0) return; + + const posOptionY = (pos.y === unit.pos.y) + ? unit.pos + : {x: unit.pos.x, y: unit.pos.y + (pos.y > unit.pos.y ? 1 : -1)} as Pos; + const posOptionX = (pos.x === unit.pos.x) + ? unit.pos + : {x: unit.pos.x + (pos.x > unit.pos.x ? 1 : -1), y: unit.pos.y} as Pos; + + let posOptionXPriority = 0; + let posOptionYPriority = 0; + + // 1. + 2. check: out-of-bounds & one axis done + if (!coreInternalIsPosValid(posOptionY) || posOptionY.y === unit.pos.y) posOptionYPriority += 500; + if (!coreInternalIsPosValid(posOptionX) || posOptionX.x === unit.pos.x) posOptionXPriority += 500; + + // 2. check: prioritize larger axis + if (Math.abs(unit.pos.x - pos.x) > Math.abs(unit.pos.y - pos.y)) + posOptionYPriority++; + else + posOptionXPriority++; + + // 3. check: pos emptiness + const posOptionXObj = coreGetObjFromPos(posOptionX); + const posOptionYObj = coreGetObjFromPos(posOptionY); + + if (posOptionXObj) posOptionXPriority += 50; + if (posOptionYObj) posOptionYPriority += 50; + + // 4. check: obj friendliness check + if (coreStaticIsFriendlyObj(posOptionXObj)) posOptionXPriority += 100; + if (coreStaticIsFriendlyObj(posOptionYObj)) posOptionYPriority += 100; + + // ----- + + if (posOptionXPriority < 250 && posOptionXPriority < posOptionYPriority) { + if (posOptionXObj && !coreStaticIsFriendlyObj(posOptionXObj)) + coreActionAttack(unit, posOptionXObj); + else if (!posOptionXObj) + coreActionMove(unit, posOptionX); + return; + } + if (posOptionYPriority < 250) { + if (posOptionYObj && !coreStaticIsFriendlyObj(posOptionYObj)) + coreActionAttack(unit, posOptionYObj); + else if (!posOptionYObj) + coreActionMove(unit, posOptionY); + } +} + export function coreActionBuild(builder: Obj, pos: Pos): void { actions.push({ type: ActionType.BUILD, diff --git a/bots/ts/client_lib/pathfinding.ts b/bots/ts/client_lib/pathfinding.ts new file mode 100644 index 00000000..b580ef45 --- /dev/null +++ b/bots/ts/client_lib/pathfinding.ts @@ -0,0 +1,148 @@ +import {getGame, coreGetObjFromPos} from './client_lib'; +import {ObjType, Pos} from './types'; + +// ---------- COST FUNCTION ---------- +function movementCostAt(pos: Pos): number { + const anyObj = coreGetObjFromPos(pos); + if (!anyObj) { + return 1; // empty tile + } else { + switch (anyObj.type) { + case ObjType.WALL: + case ObjType.CORE: + case ObjType.RESOURCE: // OBJ_DEPOSIT in C + return Infinity; // impassable + case ObjType.UNIT: + case ObjType.MONEY: // OBJ_GEM_PILE in C + case ObjType.BOMB: + return 2; // passable, but costs more + default: + return 1; // unknown object, assume passable + } + } +} + +function posToIndex(p: Pos, gridSize: number): number { + return p.y * gridSize + p.x; +} + +function indexToPos(idx: number, gridSize: number): Pos { + return { + x: idx % gridSize, + y: Math.floor(idx / gridSize) + }; +} + +function inBounds(x: number, y: number, grid: number): boolean { + return x >= 0 && y >= 0 && x < grid && y < grid; +} + +function posInBounds(p: Pos, grid: number): boolean { + return p.x >= 0 && p.y >= 0 && p.x < grid && p.y < grid; +} + +function reconstructFullPath(parent: Int32Array, startIdx: number, goalIdx: number, grid: number): Pos[] { + const path: Pos[] = []; + let cur = goalIdx; + + while (cur !== startIdx && parent[cur] !== -1) { + path.push(indexToPos(cur, grid)); + cur = parent[cur]; + } + + return path.reverse(); +} + +/** + * Returns the full path towards target using Dijkstra's algorithm. + */ +export function pathfindFullPathDijkstra(start: Pos, target: Pos): Pos[] { + const game = getGame(); + const grid = game.config.gridSize; + const total = grid * grid; + + if (grid === 0 || !posInBounds(start, grid) || !posInBounds(target, grid)) return []; + if (start.x === target.x && start.y === target.y) return []; + + const distance = new Float64Array(total).fill(Infinity); + const visited = new Uint8Array(total).fill(0); + const parent = new Int32Array(total).fill(-1); + + const startIdx = posToIndex(start, grid); + const targetIdx = posToIndex(target, grid); + distance[startIdx] = 0; + + while (true) { + let current = -1; + let bestDist = Infinity; + + for (let i = 0; i < total; ++i) { + if (visited[i] === 0 && distance[i] < bestDist) { + bestDist = distance[i]; + current = i; + } + } + + if (current === -1 || bestDist === Infinity) break; + if (current === targetIdx) break; + + visited[current] = 1; + + const curPos = indexToPos(current, grid); + const dx = [1, -1, 0, 0]; + const dy = [0, 0, 1, -1]; + + for (let dir = 0; dir < 4; ++dir) { + const nx = curPos.x + dx[dir]; + const ny = curPos.y + dy[dir]; + if (!inBounds(nx, ny, grid)) continue; + + const neighbor: Pos = {x: nx, y: ny}; + const nIdx = posToIndex(neighbor, grid); + if (visited[nIdx] === 1) continue; + + let stepCost = movementCostAt(neighbor); + // In C logic: if it's the target, cost is 1 (to be able to reach it even if it's impassable like a Core or Wall?) + // Wait, let's check pathfinding.c line 148: if (neighbor.x == target.x && neighbor.y == target.y) step_cost = 1; + if (neighbor.x === target.x && neighbor.y === target.y) stepCost = 1; + + if (stepCost === Infinity) continue; + + if (distance[current] + stepCost < distance[nIdx]) { + distance[nIdx] = distance[current] + stepCost; + parent[nIdx] = current; + } + } + } + + // Determine goal (prefer target, fallback to adjacent if unreachable) + let goalIdx = targetIdx; + + if (distance[targetIdx] === Infinity) { + let best = Infinity; + let bestIdx = -1; + + const dx = [1, -1, 0, 0]; + const dy = [0, 0, 1, -1]; + + for (let dir = 0; dir < 4; ++dir) { + const nx = target.x + dx[dir]; + const ny = target.y + dy[dir]; + if (!inBounds(nx, ny, grid)) continue; + + const adj: Pos = {x: nx, y: ny}; + const adjIdx = posToIndex(adj, grid); + + if (distance[adjIdx] !== Infinity && distance[adjIdx] < best) { + best = distance[adjIdx]; + bestIdx = adjIdx; + } + } + + if (bestIdx !== -1) goalIdx = bestIdx; + } + + if (distance[goalIdx] === Infinity) return []; + + return reconstructFullPath(parent, startIdx, goalIdx, grid); +} diff --git a/bots/ts/hardcore/gridmaster/index.ts b/bots/ts/hardcore/gridmaster/index.ts index 8fb30224..da767c08 100644 --- a/bots/ts/hardcore/gridmaster/index.ts +++ b/bots/ts/hardcore/gridmaster/index.ts @@ -1,16 +1,16 @@ import {UnitType, ObjType} from '@core/client-lib/types'; -import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = game?.objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id + const opponentCore = getGame().objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== getGame()?.my_team_id ); if (opponentCore) { - game?.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + getGame().objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame()?.my_team_id) { coreActionMove(obj, opponentCore.pos); } }); diff --git a/bots/ts/hardcore/my-core-bot/index.ts b/bots/ts/hardcore/my-core-bot/index.ts index e1e68b28..f2360950 100644 --- a/bots/ts/hardcore/my-core-bot/index.ts +++ b/bots/ts/hardcore/my-core-bot/index.ts @@ -1,16 +1,16 @@ import {UnitType, ObjType} from '@core/client-lib/types'; -import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = game?.objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id + const opponentCore = getGame().objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id ); if (opponentCore) { - game?.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + getGame().objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id) { coreActionMove(obj, opponentCore.pos); } }); diff --git a/bots/ts/softcore/gridmaster/index.ts b/bots/ts/softcore/gridmaster/index.ts index 8fb30224..c71d3ca0 100644 --- a/bots/ts/softcore/gridmaster/index.ts +++ b/bots/ts/softcore/gridmaster/index.ts @@ -1,16 +1,16 @@ import {UnitType, ObjType} from '@core/client-lib/types'; -import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = game?.objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id + const opponentCore = getGame().objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== getGame()?.my_team_id ); if (opponentCore) { - game?.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + getGame().objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id) { coreActionMove(obj, opponentCore.pos); } }); diff --git a/bots/ts/softcore/my-core-bot/index.ts b/bots/ts/softcore/my-core-bot/index.ts index 2ea4cfff..1f7f0d2a 100644 --- a/bots/ts/softcore/my-core-bot/index.ts +++ b/bots/ts/softcore/my-core-bot/index.ts @@ -1,16 +1,16 @@ import {UnitType, ObjType} from '@core/client-lib/types'; -import {game, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = game?.objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game?.my_team_id + const opponentCore = getGame().objects?.find(obj => + obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id ); if (opponentCore) { - game?.objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === game?.my_team_id) { + getGame().objects.forEach(obj => { + if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id) { coreActionMove(obj, opponentCore.pos); } }); From 8bdb759ef128578b2ded9c8652b2b63edcc52b89 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 15:07:19 +0100 Subject: [PATCH 13/15] refactor bots and client library: streamline object filtering, nearest object lookup, and pathfinding logic --- bots/ts/client_lib/client_lib.ts | 29 ++++++++++++++++++++++++ bots/ts/hardcore/gridmaster/index.ts | 32 ++++++++++++++++++--------- bots/ts/hardcore/my-core-bot/index.ts | 32 +++++++++++++++++++-------- bots/ts/softcore/gridmaster/index.ts | 32 ++++++++++++++++++--------- bots/ts/softcore/my-core-bot/index.ts | 32 +++++++++++++++++++-------- 5 files changed, 119 insertions(+), 38 deletions(-) diff --git a/bots/ts/client_lib/client_lib.ts b/bots/ts/client_lib/client_lib.ts index ed9e02de..796ae870 100644 --- a/bots/ts/client_lib/client_lib.ts +++ b/bots/ts/client_lib/client_lib.ts @@ -249,6 +249,35 @@ export function coreActionMove(unit: Obj, pos: Pos): void { }); } +export function coreGetObjsFilter(condition: (o: Obj) => boolean): Obj[] { + return getGame().objects.filter(condition); +} + +export function coreInternalDistance(pos1: Pos, pos2: Pos): number { + return Math.abs(pos1.x - pos2.x) + Math.abs(pos1.y - pos2.y); +} + +export function coreGetObjFilterNearest(pos: Pos, condition: (o: Obj) => boolean): Obj | null { + const objects = coreGetObjsFilter(condition); + if (objects.length === 0) return null; + + let nearest: Obj | null = null; + let minDistance = Infinity; + + for (const obj of objects) { + const distance = coreInternalDistance(pos, obj.pos); + if (distance < minDistance) { + minDistance = distance; + nearest = obj; + } + } + return nearest; +} + +export function coreGetObjsFilterCount(condition: (o: Obj) => boolean): number { + return coreGetObjsFilter(condition).length; +} + export function coreActionAttack(attacker: Obj, target: Obj): void { actions.push({ type: ActionType.ATTACK, diff --git a/bots/ts/hardcore/gridmaster/index.ts b/bots/ts/hardcore/gridmaster/index.ts index da767c08..55967a34 100644 --- a/bots/ts/hardcore/gridmaster/index.ts +++ b/bots/ts/hardcore/gridmaster/index.ts @@ -1,20 +1,32 @@ -import {UnitType, ObjType} from '@core/client-lib/types'; -import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter +} from '@core/client-lib/client_lib'; + +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); +} + +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = getGame().objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== getGame()?.my_team_id - ); + const opponentCore = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); if (opponentCore) { - getGame().objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame()?.my_team_id) { - coreActionMove(obj, opponentCore.pos); - } + const units = coreGetObjsFilter(isUnitOwn); + units.forEach(unit => { + coreActionPathfind(unit, opponentCore.pos); }); } } -startGame("Gridmaster TS Core Bot", onTick) +startGame("Gridmaster", onTick) diff --git a/bots/ts/hardcore/my-core-bot/index.ts b/bots/ts/hardcore/my-core-bot/index.ts index f2360950..66d96aff 100644 --- a/bots/ts/hardcore/my-core-bot/index.ts +++ b/bots/ts/hardcore/my-core-bot/index.ts @@ -1,18 +1,32 @@ -import {UnitType, ObjType} from '@core/client-lib/types'; -import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter, + coreDebugAddObjectInfo +} from '@core/client-lib/client_lib'; + +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); +} + +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = getGame().objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id - ); + const opponentCore = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); if (opponentCore) { - getGame().objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id) { - coreActionMove(obj, opponentCore.pos); - } + const units = coreGetObjsFilter(isUnitOwn); + units.forEach(unit => { + coreActionPathfind(unit, opponentCore.pos); + coreDebugAddObjectInfo(unit, `I am a warrior! ๐Ÿ—ก๏ธ - I am heading for the opponent core at [${opponentCore.pos.x},${opponentCore.pos.y}]! ๐Ÿฐ\n`); }); } } diff --git a/bots/ts/softcore/gridmaster/index.ts b/bots/ts/softcore/gridmaster/index.ts index c71d3ca0..55967a34 100644 --- a/bots/ts/softcore/gridmaster/index.ts +++ b/bots/ts/softcore/gridmaster/index.ts @@ -1,20 +1,32 @@ -import {UnitType, ObjType} from '@core/client-lib/types'; -import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter +} from '@core/client-lib/client_lib'; + +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); +} + +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = getGame().objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== getGame()?.my_team_id - ); + const opponentCore = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); if (opponentCore) { - getGame().objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id) { - coreActionMove(obj, opponentCore.pos); - } + const units = coreGetObjsFilter(isUnitOwn); + units.forEach(unit => { + coreActionPathfind(unit, opponentCore.pos); }); } } -startGame("Gridmaster TS Core Bot", onTick) +startGame("Gridmaster", onTick) diff --git a/bots/ts/softcore/my-core-bot/index.ts b/bots/ts/softcore/my-core-bot/index.ts index 1f7f0d2a..a0d56acd 100644 --- a/bots/ts/softcore/my-core-bot/index.ts +++ b/bots/ts/softcore/my-core-bot/index.ts @@ -1,18 +1,32 @@ -import {UnitType, ObjType} from '@core/client-lib/types'; -import {getGame, startGame, coreCreateUnit, coreActionMove} from '@core/client-lib/client_lib'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter, + coreDebugAddObjectInfo +} from '@core/client-lib/client_lib'; + +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); +} + +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} function onTick() { coreCreateUnit(UnitType.WARRIOR); - const opponentCore = getGame().objects?.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id - ); + const opponentCore = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); if (opponentCore) { - getGame().objects.forEach(obj => { - if (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id) { - coreActionMove(obj, opponentCore.pos); - } + const units = coreGetObjsFilter(isUnitOwn); + units.forEach(unit => { + coreActionPathfind(unit, opponentCore.pos); + coreDebugAddObjectInfo(unit, `I am a warrior! ๐Ÿ—ก๏ธ - I am heading for the opponent core at [${opponentCore.pos.x},${opponentCore.pos.y}]! ๐Ÿฐ\n`); }); } } From 1c97ad5b80a9a1e16561a1d38e5d836819c95ef2 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Sun, 15 Mar 2026 16:11:29 +0100 Subject: [PATCH 14/15] add yq installation to Dockerfile for YAML processing --- .github/workflows/my-core-bot-ts-Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile index 78546b18..a9b57485 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -5,7 +5,10 @@ FROM oven/bun:debian AS bun-base WORKDIR /app RUN apt-get update && \ - apt-get install -y make && \ + apt-get install -y make curl && \ + ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') && \ + curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${ARCH}" -o /usr/local/bin/yq && \ + chmod +x /usr/local/bin/yq && \ rm -rf /var/lib/apt/lists/* RUN bun add -g typescript From 2e3aa078bf60ce2206810865764cfa97303f8977 Mon Sep 17 00:00:00 2001 From: Emil Ebert Date: Tue, 31 Mar 2026 17:28:36 +0200 Subject: [PATCH 15/15] update Dockerfile: switch to ubuntu base, install Bun and dependencies, streamline setup --- .github/workflows/my-core-bot-ts-Dockerfile | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/my-core-bot-ts-Dockerfile b/.github/workflows/my-core-bot-ts-Dockerfile index a9b57485..667a731d 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -1,21 +1,28 @@ ARG TAG_NAME="dev" ARG SERVER_IMAGE="ghcr.io/42core-team/server" -FROM oven/bun:debian AS bun-base +FROM ubuntu:noble AS ubuntu-bun +ENV DEBIAN_FRONTEND=noninteractive WORKDIR /app -RUN apt-get update && \ - apt-get install -y make curl && \ +RUN apt update && \ + apt install -y curl ca-certificates make unzip git && \ ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') && \ curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${ARCH}" -o /usr/local/bin/yq && \ chmod +x /usr/local/bin/yq && \ rm -rf /var/lib/apt/lists/* -RUN bun add -g typescript +# Install Bun (adds to /root/.bun) +RUN curl -fsSL https://bun.sh/install | bash +ENV BUN_INSTALL=/root/.bun +ENV PATH="$BUN_INSTALL/bin:$PATH" + +# Ensure Bun is available and install TypeScript globally via Bun +RUN bun --version && bun add -g typescript FROM ${SERVER_IMAGE}:${TAG_NAME} AS game -FROM bun-base AS release +FROM ubuntu-bun AS release COPY --from=game /core/server /core/server COPY --from=game /core/data /core/data