diff --git a/.github/workflows/my-core-bot-c-Dockerfile b/.github/workflows/my-core-bot-c-Dockerfile index c3b4205..decb81e 100644 --- a/.github/workflows/my-core-bot-c-Dockerfile +++ b/.github/workflows/my-core-bot-c-Dockerfile @@ -35,3 +35,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 d46d9c3..667a731 100644 --- a/.github/workflows/my-core-bot-ts-Dockerfile +++ b/.github/workflows/my-core-bot-ts-Dockerfile @@ -1,18 +1,29 @@ ARG TAG_NAME="dev" ARG SERVER_IMAGE="ghcr.io/42core-team/server" -FROM oven/bun:latest AS bun-base +FROM ubuntu:noble AS ubuntu-bun +ENV DEBIAN_FRONTEND=noninteractive WORKDIR /app -FROM ${SERVER_IMAGE}:${TAG_NAME} AS game +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/* + +# 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" -# Build connection library from monorepo sources -FROM bun-base AS connection -WORKDIR /connection -COPY bots/ts/client_lib/ ./ +# 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 -COPY --from=connection /connection/ /core/ts/client_lib/ +COPY bots/ts/client_lib/ /core/client_lib/ \ No newline at end of file diff --git a/bots/ts/client_lib/client_lib.ts b/bots/ts/client_lib/client_lib.ts index ad6002c..796ae87 100644 --- a/bots/ts/client_lib/client_lib.ts +++ b/bots/ts/client_lib/client_lib.ts @@ -1,269 +1,419 @@ -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); - }); - }); - } +import * as net from 'node:net'; +import type {GameState, Action, Obj, Pos} from './types'; +import {ActionType, UnitType, ObjType, ObjState} from './types'; - private sendLogin(): void { - const loginMsg = { - password: "42", - id: this.teamId, - name: this.teamName - }; - this.sendJson(loginMsg); - } +let game: GameState | null = null; +let isGameInitialized: boolean = false; - 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); - } +// 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; + +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 '); + process.exit(1); } +} - 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); +async function connectInternal(): Promise { + return new Promise((resolve, reject) => { + socket = new net.Socket(); + + socket.connect(port, host, () => { + if (debug) console.log(`Connected to ${host}:${port}`); + sendLogin(); + }); + + 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) { - 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); +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 (!this.game) return; - - const id = diff.id; - if (id === undefined) return; +function handleLine(line: string): void { + try { + const parsed = JSON.parse(line); + if (debug) console.log('Received:', line); - if (diff.state === 'dead') { - this.game.objects = this.game.objects.filter(o => o.id !== id); + if (!game) { + // Initial config + game = { + elapsed_ticks: 0, + config: parsed, + 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(); 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); - } + // 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); + + // Decrement cooldowns manually as C lib does + decrementCooldowns(); - private tickCallback: ((game: GameState) => void) | null = null; + // After state update, we should have a tick + if (tickCallback) + try { + tickCallback(); + } catch (e) { + console.error('Error executing tick callback:', e); + } - public startGame(callback: (game: GameState) => void): void { - this.tickCallback = callback; + // ALWAYS send actions back to the server to prevent timeouts + sendActions(); + } catch (e) { + console.error('Error parsing JSON:', e, 'Line:', line); } +} - private sendActions(): void { - const packet = { - actions: this.actions, - debug_data: this.debugData - }; - this.sendJson(packet); - this.actions = []; - this.debugData = []; +function updateGameState(parsed: any): void { + if (!parsed) return; + const game = getGame(); + 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 createUnit(unitType: UnitType): void { - this.actions.push({ - type: ActionType.CREATE, - unit_type: unitType - }); +function printServerErrors(errors: any[]): void { + if (!errors || !Array.isArray(errors)) return; + for (const error of errors) { + console.error(`\x1b[31m${error}\x1b[0m`); } +} - public move(unit: Obj, pos: Pos): void { - this.actions.push({ - type: ActionType.MOVE, - unit_id: unit.id, - x: pos.x, - y: pos.y - }); +function decrementCooldowns(): void { + 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--; + } else if (obj.type === ObjType.CORE) { + if (obj.s_core.spawn_cooldown > 0) obj.s_core.spawn_cooldown--; + } } +} - public attack(attacker: Obj, target: Obj): void { - this.actions.push({ - type: ActionType.ATTACK, - unit_id: attacker.id, - target_id: target.id - }); +function applyDiff(diff: any): void { + 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; } - 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 - }); + const obj = findOrInitializeObject(id); + updateObjectProperties(obj, diff); + updateTypeSpecificProperties(obj, diff); +} + +function findOrInitializeObject(id: number): Obj { + let obj = getGame().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; + getGame().objects.push(obj); } + return obj; +} - public build(builder: Obj, pos: Pos): void { - this.actions.push({ - type: ActionType.BUILD, - unit_id: builder.id, - x: pos.x, - y: pos.y - }); +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 addDebugData(data: any): void { - this.debugData.push(data); +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_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); +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 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; } - entry.object_info += info; } + return nearest; +} - 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 }); +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, + 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 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, + unit_id: builder.id, + x: pos.x, + y: pos.y + }); +} - public getGame(): GameState | null { - return this.game; +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; +} + +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/client_lib/package.json b/bots/ts/client_lib/package.json index 8de2536..061e220 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/client_lib/pathfinding.ts b/bots/ts/client_lib/pathfinding.ts new file mode 100644 index 0000000..b580ef4 --- /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/configs/game.config.json b/bots/ts/hardcore/configs/game.config.json new file mode 100644 index 0000000..87cc60f --- /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 0000000..2c11aad --- /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/hardcore/gridmaster/bun.lock b/bots/ts/hardcore/gridmaster/bun.lock new file mode 100644 index 0000000..e69d99e --- /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 a46ac4a..55967a3 100644 --- a/bots/ts/hardcore/gridmaster/index.ts +++ b/bots/ts/hardcore/gridmaster/index.ts @@ -1,33 +1,32 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter +} 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); +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); } -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); +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} -bot.startGame((game: GameState) => { - // Create a warrior - bot.createUnit(UnitType.WARRIOR); +function onTick() { + 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 - ); + const opponentCore = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); 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); - } + const units = coreGetObjsFilter(isUnitOwn); + units.forEach(unit => { + coreActionPathfind(unit, opponentCore.pos); }); } -}); +} -bot.connect(); +startGame("Gridmaster", onTick) diff --git a/bots/ts/hardcore/gridmaster/package.json b/bots/ts/hardcore/gridmaster/package.json index 1d5f8e9..37d1b61 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 0000000..ba40a5e --- /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 b55a154..66d96af 100644 --- a/bots/ts/hardcore/my-core-bot/index.ts +++ b/bots/ts/hardcore/my-core-bot/index.ts @@ -1,32 +1,34 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter, + coreDebugAddObjectInfo +} 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); +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); } -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); +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} -bot.startGame((game: GameState) => { - // Create a warrior - bot.createUnit(UnitType.WARRIOR); +function onTick() { + 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 = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); 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); - } + 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`); }); } -}); +} -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 e153118..309021c 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/configs/game.config.json b/bots/ts/softcore/configs/game.config.json new file mode 100644 index 0000000..350ed10 --- /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 0000000..2c11aad --- /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 diff --git a/bots/ts/softcore/gridmaster/bun.lock b/bots/ts/softcore/gridmaster/bun.lock new file mode 100644 index 0000000..5f6e6e0 --- /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 9a0db80..55967a3 100644 --- a/bots/ts/softcore/gridmaster/index.ts +++ b/bots/ts/softcore/gridmaster/index.ts @@ -1,25 +1,32 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter +} from '@core/client-lib/client_lib'; -const bot = new ClientLib("Gridmaster TS", 1); +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); +} -bot.startGame((game: GameState) => { - // Create a warrior - bot.createUnit(UnitType.WARRIOR); +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} - // Find opponent core - const opponentCore = game.objects.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id - ); +function onTick() { + coreCreateUnit(UnitType.WARRIOR); + + const opponentCore = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); 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); - } + const units = coreGetObjsFilter(isUnitOwn); + units.forEach(unit => { + coreActionPathfind(unit, opponentCore.pos); }); } -}); +} -bot.connect(); +startGame("Gridmaster", onTick) diff --git a/bots/ts/softcore/gridmaster/package.json b/bots/ts/softcore/gridmaster/package.json index 55da2b8..36b54a2 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 0000000..061d9bb --- /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 16653cc..a0d56ac 100644 --- a/bots/ts/softcore/my-core-bot/index.ts +++ b/bots/ts/softcore/my-core-bot/index.ts @@ -1,24 +1,34 @@ -import { ClientLib } from '../../../core/ts/client_lib/client_lib'; -import { UnitType, ObjType, GameState } from '../../../core/ts/client_lib/types'; +import {UnitType, ObjType, Obj} from '@core/client-lib/types'; +import { + getGame, + startGame, + coreCreateUnit, + coreActionPathfind, + coreGetObjFilterNearest, + coreGetObjsFilter, + coreDebugAddObjectInfo +} from '@core/client-lib/client_lib'; -const bot = new ClientLib("TS Core Bot", 1); +function isCoreOpponent(obj: Obj): boolean { + return (obj.type === ObjType.CORE && obj.s_core.team_id !== getGame().my_team_id); +} -bot.startGame((game: GameState) => { - // Create a warrior - bot.createUnit(UnitType.WARRIOR); +function isUnitOwn(obj: Obj): boolean { + return (obj.type === ObjType.UNIT && obj.s_unit.team_id === getGame().my_team_id); +} - // Simple bot logic - const opponentCore = game.objects.find(obj => - obj.type === ObjType.CORE && obj.s_core.team_id !== game.my_team_id - ); +function onTick() { + coreCreateUnit(UnitType.WARRIOR); + + const opponentCore = coreGetObjFilterNearest({x: 0, y: 0}, isCoreOpponent); 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); - } + 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`); }); } -}); +} -bot.connect(); +startGame("Softcore 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 4470241..d8ea18f 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/" } }