From 6826c15b8cac21eb823b2dc111bab5c6bb8e3f56 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 13 Mar 2020 14:00:19 -0400 Subject: [PATCH 01/24] all current WIP for 1.0.0 --- constants/enums.js | 13 +++++++ engine/create-engine.js | 11 +++++- engine/create-unit.js | 47 +++++++++++++++++++++-- engine/create-world.js | 4 +- interfaces.d.ts | 16 +++++++- resources/actions.js | 14 +++++-- resources/debug.js | 7 +++- resources/map.js | 10 ++++- resources/units.js | 7 +++- systems/builder-plugin.js | 8 +++- systems/frame.js | 1 + systems/map.js | 14 +++++-- systems/unit-plugin.js | 31 ++++++++-------- systems/unit.js | 52 ++++++++++++++++++++++++++ utils/map/grid.js | 78 ++++++++++++++++++++++++++------------- 15 files changed, 251 insertions(+), 62 deletions(-) diff --git a/constants/enums.js b/constants/enums.js index 586e69a..6f59f08 100644 --- a/constants/enums.js +++ b/constants/enums.js @@ -186,6 +186,17 @@ const AbilityDataTarget = { const { valuesById: AbilityDataTargetId } = api.lookupType('AbilityData').lookupEnum('Target'); +/** + * @enum {SC2APIProtocol.CloakState} + */ +const CloakState = { + CLOAKED: 1, + CLOAKEDDETECTED: 2, + NOTCLOAKED: 3, +}; + +const { valuesById: CloakStateId } = api.lookupEnum('CloakState'); + module.exports = { AbilityDataTarget, AbilityDataTargetId, @@ -198,6 +209,8 @@ module.exports = { BuildOrder, BuildOrderId, BuildResult, + CloakState, + CloakStateId, Difficulty, DifficultyId, DisplayType, diff --git a/engine/create-engine.js b/engine/create-engine.js index b3e1cd3..f716a68 100644 --- a/engine/create-engine.js +++ b/engine/create-engine.js @@ -7,6 +7,7 @@ const argv = require('yargs') .number('StartPort') .number('GamePort') .string('LadderServer') + .string('OpponentId') .argv; const pascalCase = require('pascal-case'); // const chalk = require('chalk'); @@ -63,6 +64,7 @@ function createEngine(options = {}) { /** @type {Engine} */ const engine = { + getWorld() { return world; }, _totalLoopDelay: 0, _gameLeft: false, launcher, @@ -163,7 +165,7 @@ function createEngine(options = {}) { }; if (isManaged) { - let sPort = /** @type {number} */ argv.StartPort + 1; + let sPort = argv.StartPort + 1; participant = { ...participant, @@ -209,6 +211,10 @@ function createEngine(options = {}) { async firstRun() { const { data, resources, agent } = world; + if (isManaged) { + agent.opponent.id = argv.OpponentId; + } + /** @type {SC2APIProtocol.ResponseData} */ const gameData = await _client.data({ abilityId: true, @@ -362,6 +368,9 @@ function createEngine(options = {}) { // debug system runs last because it updates the in-client debug display return debugSystem(world); }, + shutdown() { + world.resources.get().actions._client.close(); + }, _lastRequest: null, }; diff --git a/engine/create-unit.js b/engine/create-unit.js index 3e943c1..d15b0fe 100644 --- a/engine/create-unit.js +++ b/engine/create-unit.js @@ -2,7 +2,7 @@ const UnitType = require("../constants/unit-type"); const Ability = require("../constants/ability"); -const { Alliance, WeaponTargetType, Attribute } = require("../constants/enums"); +const { Alliance, WeaponTargetType, Attribute, CloakState } = require("../constants/enums"); const { techLabTypes, reactorTypes, @@ -26,15 +26,38 @@ function createUnit(unitData, { data, resources }) { const { alliance } = unitData; + /** @type {Unit} */ const blueprint = { tag: unitData.tag, lastSeen: frame.getGameLoop(), noQueue: unitData.orders.length === 0, labels: new Map(), _availableAbilities: [], - async burrow() { + async inject(t) { + if (this.canInject()) { + let target; + if (t) { + target = t; + } else { + const [idleHatch] = units.getById(UnitType.HATCH).filter(u => u.isIdle()); + if (idleHatch) { + target = idleHatch; + } else { + return; + } + } + + return actions.do(Ability.EFFECT_INJECTLARVA, this.tag, { target }); + } + }, + async blink(target, opts = {}) { + if (this.canBlink()) { + return actions.do(Ability.EFFECT_BLINK, this.tag, { target, ...opts }); + } + }, + async burrow(opts = {}) { if (this.is(UnitType.WIDOWMINE)) { - return actions.do(Ability.BURROWDOWN, this.tag); + return actions.do(Ability.BURROWDOWN, this.tag, opts); } }, async toggle(options = {}) { @@ -68,6 +91,9 @@ function createUnit(unitData, { data, resources }) { getLabel(name) { return this.labels.get(name); }, + getLife() { + return this.health / this.healthMax * 100; + }, abilityAvailable(id) { return this._availableAbilities.includes(id); }, @@ -80,12 +106,18 @@ function createUnit(unitData, { data, resources }) { is(type) { return this.unitType === type; }, + isCloaked() { + return this.cloak !== CloakState.NOTCLOAKED; + }, isConstructing() { return this.orders.some(o => constructionAbilities.includes(o.abilityId)); }, isCombatUnit() { return combatTypes.includes(this.unitType); }, + isEnemy() { + return this.alliance === Alliance.ENEMY; + }, isFinished() { return this.buildProgress >= 1; }, @@ -130,6 +162,9 @@ function createUnit(unitData, { data, resources }) { // if this unit wasn't updated this frame, this will be false return this.lastSeen === frame.getGameLoop(); }, + isIdle() { + return this.noQueue; + }, isStructure() { return this.data().attributes.includes(Attribute.STRUCTURE); }, @@ -141,6 +176,12 @@ function createUnit(unitData, { data, resources }) { const addon = units.getByTag(this.addOnTag); return techLabTypes.includes(addon.unitType); }, + canInject() { + return this.abilityAvailable(Ability.EFFECT_INJECTLARVA); + }, + canBlink() { + return this.abilityAvailable(Ability.EFFECT_BLINK) || this.abilityAvailable(Ability.EFFECT_BLINK_STALKER); + }, canMove() { return this._availableAbilities.includes(Ability.MOVE); }, diff --git a/engine/create-world.js b/engine/create-world.js index 8c8b49e..687c5df 100644 --- a/engine/create-world.js +++ b/engine/create-world.js @@ -10,7 +10,7 @@ const MapManager = require('../resources/map'); const UnitManager = require('../resources/units'); /** @returns {World} */ -function createWorld() { +function createWorld(client) { const world = { agent: null, data: null, @@ -26,7 +26,7 @@ function createWorld() { debug: Debugger(world), units: UnitManager(world), events: EventChannel(world), - actions: ActionManager(world), + actions: ActionManager(world, client), }); return world; diff --git a/interfaces.d.ts b/interfaces.d.ts index 8ff66d4..2e4cc55 100644 --- a/interfaces.d.ts +++ b/interfaces.d.ts @@ -104,7 +104,8 @@ interface SystemWrapper { _system: System; } -type Enemy = { +type Opponent = { + id: string; race: SC2APIProtocol.Race; } @@ -122,7 +123,7 @@ interface Agent extends PlayerData { canAffordUpgrade: (upgradeId: number) => boolean; hasTechFor: (unitTypeId: number) => boolean; race?: SC2APIProtocol.Race; - enemy?: Enemy; + opponent?: Opponent; settings: SC2APIProtocol.PlayerSetup; systems: SystemWrapper[]; use: (sys: (SystemWrapper | SystemWrapper[])) => void; @@ -140,25 +141,36 @@ interface Unit extends SC2APIProtocol.Unit { availableAbilities: () => Array; data: () => SC2APIProtocol.UnitTypeData; is: (unitType: UnitTypeId) => boolean; + isCloaked: () => boolean; + isConstructing: () => boolean; isCombatUnit: () => boolean; + isEnemy: () => boolean; isFinished: () => boolean; isWorker: () => boolean; isTownhall: () => boolean; isGasMine: () => boolean; isMineralField: () => boolean; isStructure: () => boolean; + isIdle: () => boolean; isCurrent: () => boolean; isHolding: () => boolean; isGathering: (type?: 'minerals' | 'vespene') => boolean; + isReturning: () => boolean; hasReactor: () => boolean; hasTechLab: () => boolean; hasNoLabels: () => boolean; + canInject: () => boolean; + canBlink: () => boolean; canMove: () => boolean; canShootUp: () => boolean; update: (unit: SC2APIProtocol.Unit) => void; + inject: (target?: Unit) => Promise; + blink: (target: Point2D, opts: AbilityOptions) => Promise; toggle: (options: AbilityOptions) => Promise; + burrow: (options: AbilityOptions) => Promise; addLabel: (name: string, value: any) => Map; hasLabel: (name: string) => boolean; + getLife: () => number; getLabel: (name: string) => any; removeLabel: (name: string) => boolean; } diff --git a/resources/actions.js b/resources/actions.js index 1f13511..72535d6 100644 --- a/resources/actions.js +++ b/resources/actions.js @@ -27,13 +27,17 @@ function getRandomN(arr, n) { * @returns {ActionManager} * @param {World} world */ -function createActionManager(world) { - const protoClient = createTransport(); +function createActionManager(world, client) { + const protoClient = client || createTransport(); return { _client: protoClient, async do(abilityId, ts, opts = {}) { - const tags = Array.isArray(ts) ? ts : [ts]; + let tags = Array.isArray(ts) ? ts : [ts]; + + if (tags[0].tag) { + tags = tags.map(u => u.tag); + } /** @type {SC2APIProtocol.ActionRawUnitCommand} */ const doAction = { @@ -57,7 +61,9 @@ function createActionManager(world) { return res; }); }, - async smart(units, pos, queue = false) { + async smart(us, pos, queue = false) { + const units = Array.isArray(us) ? us : [us]; + const smartTo = { abilityId: Ability.SMART, unitTags: units.map(u => u.tag), diff --git a/resources/debug.js b/resources/debug.js index cb3b6ce..64c2da6 100644 --- a/resources/debug.js +++ b/resources/debug.js @@ -1,6 +1,7 @@ "use strict"; // eslint-disable-next-line +const debugWidget = require('debug')('sc2:DebugWidget'); const Color = require('../constants/color'); const getRandom = require('../utils/get-random'); const { cellsInFootprint } = require('../utils/geometry/plane'); @@ -22,8 +23,10 @@ function createDebugger(world) { updateScreen() { const { actions: { _client } } = world.resources.get(); - this._drawDebugWidget(widgetData); - + if (debugWidget.enabled) { + this._drawDebugWidget(widgetData); + } + const debugCommands = Object.values(commands).reduce((commands, command) => { return commands.concat(command); }, []); diff --git a/resources/map.js b/resources/map.js index 5d11fae..4fbe9f2 100644 --- a/resources/map.js +++ b/resources/map.js @@ -3,7 +3,7 @@ const PF = require('pathfinding'); const debugWeights = require('debug')('sc2:silly:DebugWeights'); const { enums: { Alliance }, Color } = require('../constants'); -const { add, distance, avgPoints, closestPoint } = require('../utils/geometry/point'); +const { add, distance, avgPoints, closestPoint, areEqual } = require('../utils/geometry/point'); const { gridsInCircle } = require('../utils/geometry/angle'); const { cellsInFootprint } = require('../utils/geometry/plane'); const { gasMineTypes } = require('../constants/groups'); @@ -76,6 +76,10 @@ function createMapManager(world) { const point = createPoint2D(p); return !!this._mapState.visibility[point.y][point.x]; }, + isRamp(p) { + const point = createPoint2D(p); + return !!this._ramps.find(c => areEqual(point, c)); + }, hasCreep(p) { const point = createPoint2D(p); return !!this._mapState.creep[point.y][point.x]; @@ -185,6 +189,8 @@ function createMapManager(world) { return this._grids.height[point.y][point.x] / 10; }, getCombatRally() { + if (this.isCustom()) return this.getCenter(); + const numOfBases = this.getOccupiedExpansions().length; if (combatRally && combatRally.numOfBases === numOfBases ) return combatRally.pos; @@ -258,6 +264,8 @@ function createMapManager(world) { if (graph) { this._graph = graph; } else { + // @WIP: uncomment for maybe useful debugging? + // console.log(this._mapSize.x, this._mapSize.y, this._grids.pathing.length, this._grids.pathing[0].length) const newGraph = new PF.Grid(this._mapSize.x, this._mapSize.y, this._grids.pathing); newGraph.nodes.forEach((row) => { row.forEach((node) => { diff --git a/resources/units.js b/resources/units.js index aa985ed..2e303f9 100644 --- a/resources/units.js +++ b/resources/units.js @@ -96,6 +96,8 @@ function createUnits(world) { } return filterArr(theUnits, filter); + } else if (typeof filter === 'function') { + return this.getAll().filter(filter); } else if (typeof filter === 'number') { return Array.from(this._units[filter].values()); } else { @@ -124,7 +126,7 @@ function createUnits(world) { }, getCombatUnits(filter = Alliance.SELF) { return this.getAlive(filter) - .filter(u => combatTypes.includes(u.unitType)); + .filter(u => u.isCombatUnit()); }, getRangedCombatUnits() { return this.getCombatUnits() @@ -142,6 +144,9 @@ function createUnits(world) { return workers.filter(u => !u.labels.has('command')); } }, + getIdle() { + return this.getAlive(Alliance.SELF).filter(u => u.noQueue); + }, getIdleWorkers() { return this.getWorkers().filter(w => w.noQueue); }, diff --git a/systems/builder-plugin.js b/systems/builder-plugin.js index f2ad560..0d66aa3 100644 --- a/systems/builder-plugin.js +++ b/systems/builder-plugin.js @@ -1,6 +1,7 @@ 'use strict'; const debugBuild = require('debug')('sc2:debug:build'); +const debugDraw = require('debug')('sc2:DrawDebug'); const debugBuildSilly = require('debug')('sc2:silly:build'); const { distance, distanceX, distanceY } = require('../utils/geometry/point'); const getRandom = require('../utils/get-random'); @@ -172,12 +173,15 @@ function builderPlugin(system) { } if (buildTask.touched === false) { - debugBuild(`starting new build task: %o`, buildTask); + const taskName = buildTask.type === 'ability' ? AbilityId[buildTask.id] + : buildTask.type === 'upgrade' ? UpgradeId[buildTask.id] + : UnitTypeId[buildTask.id]; + debugBuild(`starting new build task: ${buildTask.type} ${taskName}`); buildTask.started = gameLoop; buildTask.touched = true; } - if (debugBuild.enabled) { + if (debugDraw.enabled) { world.resources.get().debug.setDrawTextScreen('buildOrder', [{ pos: { x: 0.85, y: 0.1 }, text: `Build:\n\n${this.state[buildSym].map((buildTask, i) => { diff --git a/systems/frame.js b/systems/frame.js index d1933a2..70309dc 100644 --- a/systems/frame.js +++ b/systems/frame.js @@ -22,6 +22,7 @@ const frameSystem = { ]); frame._gameInfo = gameInfo; + frame._gameLoop = responseObservation.observation.gameLoop; frame._observation = responseObservation.observation; }, async onStep({ resources }) { diff --git a/systems/map.js b/systems/map.js index a57286c..f16f15a 100644 --- a/systems/map.js +++ b/systems/map.js @@ -21,7 +21,7 @@ const { cellsInFootprint } = require('../utils/geometry/plane'); const { Alliance } = require('../constants/enums'); const { MapDecompositionError } = require('../engine/errors'); const Color = require('../constants/color'); -const { UnitTypeId } = require('../constants/'); +const { UnitType, UnitTypeId } = require('../constants'); const { vespeneGeyserTypes, unbuildablePlateTypes, mineralFieldTypes } = require('../constants/groups'); /** @@ -161,6 +161,7 @@ function calculateWall(world, expansion) { const deadDiagNeighbors = diagNeighbors.filter(({ x, y }) => pathing[y][x] === 1); if ((deadNeighbors.length <= 0) && (deadDiagNeighbors.length <= 0)) { + // @FIXME: this is legitimately the worst possible way to check for a ramp if (neighbors.filter(({ x, y }) => miniMap[y][x] === 114).length <= 0) { decomp.liveHull.push({ x, y }); } else { @@ -229,7 +230,7 @@ function calculateWall(world, expansion) { }) .map(wall => wall.filter(cell => map.isPlaceable(cell))) .sort((a, b) => a.length - b.length) - .filter(wall => wall.length >= (live.length)) + .filter(wall => wall.length >= live.length - 3) .filter (wall => distance(avgPoints(wall), avgPoints(live)) <= live.length) .filter((wall, i, arr) => wall.length === arr[0].length); @@ -311,6 +312,9 @@ function calculateExpansions(world) { const mapSystem = { name: 'MapSystem', type: 'engine', + defaultOptions: { + stepIncrement: 1, + }, async onGameStart(world) { const { units, frame, map, debug } = world.resources.get(); const { startRaw } = frame.getGameInfo(); @@ -512,7 +516,11 @@ const mapSystem = { async onUnitDestroyed({ resources }, deadUnit) { const { map } = resources.get(); - if (deadUnit.isStructure()) { + if (deadUnit.isStructure() && !deadUnit.isGasMine()) { + if (deadUnit.is(UnitType.PYLON)) { + // @WIP: unsure what this was debugging for, but leaving commented for posterity until i figure it out + // console.log(resources.get().frame.getGameLoop(), `pylon destroyed (or canceled?) progress is/was: ${deadUnit.buildProgress}`); + } const { pos } = deadUnit; const footprint = getFootprint(deadUnit.unitType); diff --git a/systems/unit-plugin.js b/systems/unit-plugin.js index 5804783..e2e2477 100644 --- a/systems/unit-plugin.js +++ b/systems/unit-plugin.js @@ -29,9 +29,12 @@ function unitPlugin(system) { const systemUnits = unitsFilter.reduce((sysUnits, unitType) =>{ sysUnits[unitType] = units.getById(unitType) .filter(u => u.isFinished()) - .filter(systemUnit => { + .filter((systemUnit) => { const unitTag = systemUnit.tag; - return Object.values(labeledUnits).every(labelUnit => labelUnit.tag !== unitTag); + const labeledTags = Object.values(labeledUnits) + .reduce((pool, arr) => pool.concat(arr), []) + .map(lu => lu.tag); + return !labeledTags.includes(unitTag); }); return sysUnits; }, {}); @@ -55,25 +58,21 @@ function unitPlugin(system) { const onIdleFunctions = system.idleFunctions || {}; system.onUnitIdle = async function(world, unit) { - labelsFilter.forEach((label) => { + const foundLabelFn = labelsFilter.find((label) => { if (unit.hasLabel(label)) { - return onIdleFunctions[label] ? - reflect(onIdleFunctions[label], world, unit) : - onIdleFunctions.labeled ? - reflect(onIdleFunctions.labeled, world, unit, label) : - onIdleFunctions[unit.unitType] ? - reflect(onIdleFunctions[unit.unitType], world, unit) : - undefined; + return !!onIdleFunctions[label]; } }); - if (unitsFilter.includes(unit.unitType)) { - if (onIdleFunctions[unit.unitType]) { - return reflect(onIdleFunctions[unit.unitType], world, unit); - } + if (foundLabelFn) { + return reflect(onIdleFunctions[foundLabelFn], world, unit); + } else if (!unit.hasNoLabels() && onIdleFunctions.labeled) { + return reflect(onIdleFunctions.labeled, world, unit, [...unit.labels.keys()][0]); + } else if (onIdleFunctions[unit.unitType]) { + return reflect(onIdleFunctions[unit.unitType], world, unit); + } else { + return systemOnUnitIdle(world, unit); } - - return systemOnUnitIdle(world, unit); }; system.onUnitCreated = async function(world, unit) { diff --git a/systems/unit.js b/systems/unit.js index 3248dd4..2d579e4 100644 --- a/systems/unit.js +++ b/systems/unit.js @@ -62,6 +62,24 @@ const unitSystem = { ); }; + /** + * Test for a unit transforming type + * @param {SC2APIProtocol.Unit} incData + * @param {Unit} currentUnit + */ + const isTransforming = (incData, currentUnit) => { + return currentUnit.unitType !== incData.unitType; + }; + + /** + * Test for a unit finishing a burrow + * @param {SC2APIProtocol.Unit} incData + * @param {Unit} currentUnit + */ + const hasBurrowed = (incData, currentUnit) => { + return incData.isBurrowed && !currentUnit.isBurrowed; + }; + /** * Test for unit entering a skirmish * @param {SC2APIProtocol.Unit} incData @@ -108,6 +126,15 @@ const unitSystem = { data: currentUnit, type: 'all', }); + + // the unit is finished, but it's also idle + if (unitData.orders.length > 0) { + events.write({ + name: "unitIdle", + data: currentUnit, + type: 'all', + }); + } } if (isIdle(unitData, currentUnit)) { @@ -118,6 +145,22 @@ const unitSystem = { }); } + if (isTransforming(unitData, currentUnit)) { + events.write({ + name: "unitIsTransforming", + data: currentUnit, + type: 'all', + }); + } + + if (hasBurrowed(unitData, currentUnit)) { + events.write({ + name: "unitHasBurrowed", + data: currentUnit, + type: 'all', + }); + } + if (hasEngaged(unitData, currentUnit)) { events.write({ name: "unitHasEngaged", @@ -165,6 +208,15 @@ const unitSystem = { data: newUnit, type: 'all', }); + + // if it's unrallied, then it's also idle + if (unitData.orders.length > 0) { + events.write({ + name: "unitIdle", + data: newUnit, + type: 'all', + }); + } } else if (unitData.alliance === Alliance.ENEMY) { events.write({ name: "enemyFirstSeen", diff --git a/utils/map/grid.js b/utils/map/grid.js index d788cc2..e0839eb 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -1,6 +1,7 @@ 'use strict'; const chalk = require('chalk'); +const Uint1Array = require('uint1array'); /** * @param {Grid2D} grid @@ -29,20 +30,43 @@ function debugGrid(grid) { }); } +/** + * @param {Uint8Array} buffer + * @param {number} i + * @param {number} bit + */ +function readSingleBit(buffer, i, bit){ + return (buffer[i] >> bit) % 2; + } + /** * @param {SC2APIProtocol.ImageData} imageData * @param {number} width * @returns {Grid2D} */ -function consumeImageData(imageData, width) { +function consumeImageData(imageData, width, height = Infinity) { + // @WIP: none of this actually works... this debugger statement might help + // debugger; + if (!width) { + throw new Error('Map width needed to digest raw grids!'); + } + + const BYTE_LENGTH = imageData.data.byteLength; + /* new fast code, 0.02ms per digestion */ const arrayBuffer = imageData.data.buffer; const result = []; let i = 0; - - while (i < imageData.data.byteLength) { - result.push(new Uint8Array(arrayBuffer, i + imageData.data.byteOffset, width)); + + while (i < BYTE_LENGTH) { + if (result.length >= height) { + break; + } + if (arrayBuffer.byteLength < (imageData.data.byteOffset + (i * 8))) { + break; + } + result.push(new Uint1Array(arrayBuffer, (i * 8) + imageData.data.byteOffset, width)); i += width; } @@ -64,11 +88,15 @@ function consumeImageData(imageData, width) { */ function consumeRawGrids(raw) { const { mapSize, placementGrid: plGrid, pathingGrid: paGrid, terrainHeight: teGrid } = raw; - const width = mapSize.x; + const { x, y } = mapSize; + + // @WIP: more trying to log / debug things... maybe helpful? + // console.log(paGrid, paGrid.data.buffer); + const placementGrid2D = consumeImageData(plGrid, x, y); + const pathingGrid2D = consumeImageData(paGrid, x, y); + const heightGrid2D = consumeImageData(teGrid, x, y); - const placementGrid2D = consumeImageData(plGrid, width); - const pathingGrid2D = consumeImageData(paGrid, width); - const heightGrid2D = consumeImageData(teGrid, width); + // console.log(pathingGrid2D) const height = heightGrid2D.map((row) => { return row.map(tile => { @@ -81,31 +109,31 @@ function consumeRawGrids(raw) { }); }); - const placement = placementGrid2D.map((row) => { - return row.map(pixel => { - return pixel === 255 ? 1 : 0; - }); - }); + // const placement = placementGrid2D.map((row) => { + // return row.map(pixel => { + // return pixel === 255 ? 1 : 0; + // }); + // }); - //debugGrid(placement); + debugGrid(placementGrid2D); - const pathing = pathingGrid2D.map((row) => { - return row.map(pixel => { - return pixel === 255 ? 1 : 0; - }); - }); + // const pathing = pathingGrid2D.map((row) => { + // return row.map(pixel => { + // return pixel === 255 ? 1 : 0; + // }); + // }); - //debugGrid(pathing); + debugGrid(pathingGrid2D); return { height, - placement, - pathing, - miniMap: placement.map((row, y) => { + placement: placementGrid2D, + pathing: pathingGrid2D, + miniMap: placementGrid2D.map((row, y) => { return row.map((pixel, x) => { - if (pixel === 1 && pathing[y][x] === 1) { + if (pixel === 1 && pathingGrid2D[y][x] === 1) { return 66; - } else if (pixel === 0 && pathing[y][x] === 0) { + } else if (pixel === 0 && pathingGrid2D[y][x] === 0) { return 114; } else { return pixel; From adce3b4bbf982f6c979188f554837f574cb114bf Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Sun, 15 Mar 2020 21:38:47 -0400 Subject: [PATCH 02/24] sort of displaying something --- agent/create-agent.js | 2 +- interfaces.d.ts | 1 + package.json | 1 + resources/map.js | 3 ++- utils/map/grid.js | 51 ++++++++++++++++++++++++++++--------------- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/agent/create-agent.js b/agent/create-agent.js index eadc322..5e21c54 100644 --- a/agent/create-agent.js +++ b/agent/create-agent.js @@ -36,7 +36,7 @@ function createAgent(blueprint = {}) { race: Race.RANDOM, ...blueprint.settings, }, - interface: blueprint.interface || { raw: true }, + interface: blueprint.interface || { raw: true, /* rawCropToPlayableArea: true */ }, canAfford(unitTypeId, earmarkName) { const { data } = this._world; const { minerals, vespene } = this; diff --git a/interfaces.d.ts b/interfaces.d.ts index 2e4cc55..9203c15 100644 --- a/interfaces.d.ts +++ b/interfaces.d.ts @@ -596,5 +596,6 @@ interface Engine { dispatch: () => Promise; systems: SystemWrapper[]; firstRun: () => Promise + getWorld: () => World onGameEnd: (results: SC2APIProtocol.PlayerResult[]) => GameResult; } diff --git a/package.json b/package.json index ef12b14..57b1578 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "protobufjs": "^6.8.8", "range": "0.0.3", "shortid": "^2.2.14", + "uint1array": "^1.1.1", "yargs": "^12.0.5" } } diff --git a/resources/map.js b/resources/map.js index 4fbe9f2..5859ff0 100644 --- a/resources/map.js +++ b/resources/map.js @@ -265,7 +265,8 @@ function createMapManager(world) { this._graph = graph; } else { // @WIP: uncomment for maybe useful debugging? - // console.log(this._mapSize.x, this._mapSize.y, this._grids.pathing.length, this._grids.pathing[0].length) + console.log(this._mapSize.x, this._mapSize.y, this._grids.pathing.length, this._grids.pathing[0].length) + // console.log(this._grids.pathing, this._grids.pathing[0]) const newGraph = new PF.Grid(this._mapSize.x, this._mapSize.y, this._grids.pathing); newGraph.nodes.forEach((row) => { row.forEach((node) => { diff --git a/utils/map/grid.js b/utils/map/grid.js index e0839eb..a353930 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -1,19 +1,28 @@ 'use strict'; const chalk = require('chalk'); -const Uint1Array = require('uint1array'); +const Uint1Array = require('uint1array').default; /** * @param {Grid2D} grid */ -function debugGrid(grid) { +function debugGrid(grid, playable) { + const pointInRect = ({p0, p1}, x, y) => ( + (x > p0.x && x < p1.x) && (y > p0.y && y < p1.y) + ); + const displayGrid = grid.slice(); - displayGrid.reverse().forEach((row) => { + + displayGrid.forEach((row, x) => { console.log( - row.map((pixel) => { + [...row].map((pixel, y) => { switch(pixel) { case 0: - return ' '; + if (!pointInRect(playable, x, y)) { + return chalk.bgRed` `; + } else { + return chalk.bgGreen` `; + } case 1: return '░'; case 104: @@ -45,13 +54,14 @@ function readSingleBit(buffer, i, bit){ * @returns {Grid2D} */ function consumeImageData(imageData, width, height = Infinity) { - // @WIP: none of this actually works... this debugger statement might help - // debugger; if (!width) { throw new Error('Map width needed to digest raw grids!'); } + const BYTE_OFFSET = imageData.data.byteOffset; const BYTE_LENGTH = imageData.data.byteLength; + const WIDTH_IN_BYTES = width * 0.125; + // const BITS_PER_PIXEL = imageData.bitsPerPixel; /* new fast code, 0.02ms per digestion */ const arrayBuffer = imageData.data.buffer; @@ -63,14 +73,18 @@ function consumeImageData(imageData, width, height = Infinity) { if (result.length >= height) { break; } - if (arrayBuffer.byteLength < (imageData.data.byteOffset + (i * 8))) { + + if (arrayBuffer.byteLength < (BYTE_OFFSET + i )) { break; } - result.push(new Uint1Array(arrayBuffer, (i * 8) + imageData.data.byteOffset, width)); - i += width; + + const row = new Uint1Array(arrayBuffer, i + BYTE_OFFSET, WIDTH_IN_BYTES); + result.push(row); + i += WIDTH_IN_BYTES; } - - return result.reverse(); + + return result; + // return result.reverse(); /* old slow code, ~2ms per digestion */ // const gridarr = [...imageData.data]; @@ -90,13 +104,16 @@ function consumeRawGrids(raw) { const { mapSize, placementGrid: plGrid, pathingGrid: paGrid, terrainHeight: teGrid } = raw; const { x, y } = mapSize; + // console.log(mapSize, plGrid, paGrid) // @WIP: more trying to log / debug things... maybe helpful? - // console.log(paGrid, paGrid.data.buffer); + const placementGrid2D = consumeImageData(plGrid, x, y); + debugGrid(placementGrid2D, raw.playableArea); + const pathingGrid2D = consumeImageData(paGrid, x, y); - const heightGrid2D = consumeImageData(teGrid, x, y); + - // console.log(pathingGrid2D) + const heightGrid2D = consumeImageData(teGrid, x, y); const height = heightGrid2D.map((row) => { return row.map(tile => { @@ -115,7 +132,7 @@ function consumeRawGrids(raw) { // }); // }); - debugGrid(placementGrid2D); + // const pathing = pathingGrid2D.map((row) => { // return row.map(pixel => { @@ -123,8 +140,6 @@ function consumeRawGrids(raw) { // }); // }); - debugGrid(pathingGrid2D); - return { height, placement: placementGrid2D, From f94291dfa620e27f8e141f6d8b2e1a31c7d00c27 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 1 May 2020 18:59:56 -0400 Subject: [PATCH 03/24] alpha release commit: fixed pathfinding fixed wall calcs stuff is *really* slow, probably, until grid digestion is fixed for real sc2:DebugWeights sc2:DebugPathable --- agent/create-agent.js | 2 +- package.json | 2 +- resources/map.js | 30 ++++++++- systems/map.js | 11 +-- utils/map/grid.js | 152 ++++++++++++++++++++++++------------------ 5 files changed, 124 insertions(+), 73 deletions(-) diff --git a/agent/create-agent.js b/agent/create-agent.js index 5e21c54..8fe2e9d 100644 --- a/agent/create-agent.js +++ b/agent/create-agent.js @@ -36,7 +36,7 @@ function createAgent(blueprint = {}) { race: Race.RANDOM, ...blueprint.settings, }, - interface: blueprint.interface || { raw: true, /* rawCropToPlayableArea: true */ }, + interface: blueprint.interface || { raw: true, rawCropToPlayableArea: true }, canAfford(unitTypeId, earmarkName) { const { data } = this._world; const { minerals, vespene } = this; diff --git a/package.json b/package.json index 57b1578..c5400d0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@node-sc2/proto": "^0.5.7", "bluebird": "^3.5.3", "bresenham": "0.0.4", - "chalk": "^2.4.1", + "chalk": "^3.0.0", "convert-hrtime": "^2.0.0", "create-error": "^0.3.1", "debug": "^4.1.0", diff --git a/resources/map.js b/resources/map.js index 5859ff0..b1150bc 100644 --- a/resources/map.js +++ b/resources/map.js @@ -1,7 +1,8 @@ 'use strict'; const PF = require('pathfinding'); -const debugWeights = require('debug')('sc2:silly:DebugWeights'); +const debugWeights = require('debug')('sc2:DebugWeights'); +const debugPathable = require('debug')('sc2:DebugPathable'); const { enums: { Alliance }, Color } = require('../constants'); const { add, distance, avgPoints, closestPoint, areEqual } = require('../utils/geometry/point'); const { gridsInCircle } = require('../utils/geometry/angle'); @@ -140,6 +141,11 @@ function createMapManager(world) { closestPathable(point, r = 3) { const allPathable = gridsInCircle(point, r, { normalize: true }) .filter(p => this.isPathable(p)); + + if (allPathable.length <= 0) { + throw new Error(`No pathable points within ${r} radius of point ${point}`); + } + return closestPoint(point, allPathable); }, /** @@ -157,7 +163,9 @@ function createMapManager(world) { distance: this.path(startPoint, add(expansion.townhallPosition, 3)).length, }; }) - .filter(exp => exp.distance > 0) + .filter((exp) => { + return exp.distance > 0; + }) .sort((a, b) => a.distance - b.distance)[0]; return expansionOrder[closestIndex]; @@ -232,6 +240,22 @@ function createMapManager(world) { setGrids(newGrids) { // merging allows to update partial grids, or individual this._grids = { ...this._grids, ...newGrids }; + + if (debugPathable.enabled) { + world.resources.get().debug.setDrawCells('debugPathable', this._grids.pathing.reduce((cells, row, y) => { + row.forEach((node, x) => { + if (this.isPathable({x, y})) { + cells.push({ + pos: {x, y}, + text: `p-able`, + color: Color.LIME_GREEN, + }); + } + }); + + return cells; + }, [])); + } }, setSize(mapSize) { this._mapSize = mapSize; @@ -265,7 +289,7 @@ function createMapManager(world) { this._graph = graph; } else { // @WIP: uncomment for maybe useful debugging? - console.log(this._mapSize.x, this._mapSize.y, this._grids.pathing.length, this._grids.pathing[0].length) + // console.log(this._mapSize.x, this._mapSize.y, this._grids.pathing.length, this._grids.pathing[0].length) // console.log(this._grids.pathing, this._grids.pathing[0]) const newGraph = new PF.Grid(this._mapSize.x, this._mapSize.y, this._grids.pathing); newGraph.nodes.forEach((row) => { diff --git a/systems/map.js b/systems/map.js index f16f15a..b0a602d 100644 --- a/systems/map.js +++ b/systems/map.js @@ -221,7 +221,7 @@ function calculateWall(world, expansion) { const first = wall[0]; const last = wall[wall.length -1]; - const newGraph = map.newGraph(map._grids.placement.map(row => row.map(cell => cell === 0 ? 1 : 0))); + const newGraph = map.newGraph(map._grids.pathing); newGraph.setWalkableAt(first.x, first.y, true); newGraph.setWalkableAt(last.x, last.y, true); @@ -270,7 +270,7 @@ function calculateExpansions(world) { // @TODO: handle the case of more than 1 enemy location const pathingEL = createPoint2D(map.getLocations().enemy[0]); pathingEL.x = pathingEL.x + 3; - + let expansions; try { expansions = findClusters([...mineralFields, ...vespeneGeysers]) @@ -278,7 +278,6 @@ function calculateExpansions(world) { .map((expansion) => { const start = { ...expansion.townhallPosition }; start.x = start.x + 3; - const paths = { pathFromMain: map.path(start, pathingSL), pathFromEnemy: map.path(start, pathingEL), @@ -362,6 +361,10 @@ const mapSystem = { // debug.setDrawCells('ramps', map._ramps.map(r => ({ pos: r })), { size: 0.5, cube: true }); calculateExpansions(world); + + /** + * i guess... don't uncomment this until it's fixed? + */ calculateWall(world, map.getNatural()); }, async onStep({ data, resources }) { @@ -463,7 +466,7 @@ const mapSystem = { row.forEach((placeable, x) => { cells.push({ pos: { x, y }, - size: 0.2, + size: 0.5, color: placeable ? Color.LIME_GREEN : Color.RED, }); }); diff --git a/utils/map/grid.js b/utils/map/grid.js index a353930..7ca38f0 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -3,19 +3,22 @@ const chalk = require('chalk'); const Uint1Array = require('uint1array').default; - /** - * @param {Grid2D} grid - */ -function debugGrid(grid, playable) { +/** + * @param {Grid2D} grid + */ +function debugGrid(grid, playable, height = false) { const pointInRect = ({p0, p1}, x, y) => ( (x > p0.x && x < p1.x) && (y > p0.y && y < p1.y) ); - const displayGrid = grid.slice(); + const displayGrid = grid.slice().reverse(); displayGrid.forEach((row, x) => { console.log( [...row].map((pixel, y) => { + if (height === true) { + return chalk.rgb(0, pixel, 0)('X'); + } switch(pixel) { case 0: if (!pointInRect(playable, x, y)) { @@ -25,6 +28,10 @@ function debugGrid(grid, playable) { } case 1: return '░'; + case 66: + return chalk.bgBlue` `; + case 114: + return ' '; case 104: // @ts-ignore return chalk.bgGreen`░`; @@ -46,7 +53,50 @@ function debugGrid(grid, playable) { */ function readSingleBit(buffer, i, bit){ return (buffer[i] >> bit) % 2; - } +} + +// /** +// * @param {SC2APIProtocol.ImageData} imageData +// * @param {number} width +// * @returns {Grid2D} +// */ +// function consumeImageData(imageData, width, height = Infinity) { +// if (!width) { +// throw new Error('Map width needed to digest raw grids!'); +// } + +// const BYTE_OFFSET = imageData.data.byteOffset; +// const BYTE_LENGTH = imageData.data.byteLength; +// const BITS_PER_PIXEL = imageData.bitsPerPixel; +// const WIDTH_IN_BYTES = BITS_PER_PIXEL === 1 +// ? width * 0.125 +// : width; + +// /* new fast code, 0.02ms per digestion */ +// const arrayBuffer = imageData.data.buffer; + +// const View = BITS_PER_PIXEL === 1 +// ? Uint1Array +// : Uint8Array; + +// const result = []; +// let i = 0; + +// while (i < BYTE_LENGTH) { +// if (result.length >= height) { +// break; +// } + +// if (arrayBuffer.byteLength < (BYTE_OFFSET + i )) { +// break; +// } + +// result.push(new View(arrayBuffer, i + BYTE_OFFSET, WIDTH_IN_BYTES)); +// i += WIDTH_IN_BYTES; +// } + +// return result.reverse(); +// } /** * @param {SC2APIProtocol.ImageData} imageData @@ -58,41 +108,27 @@ function consumeImageData(imageData, width, height = Infinity) { throw new Error('Map width needed to digest raw grids!'); } - const BYTE_OFFSET = imageData.data.byteOffset; - const BYTE_LENGTH = imageData.data.byteLength; - const WIDTH_IN_BYTES = width * 0.125; - // const BITS_PER_PIXEL = imageData.bitsPerPixel; - - /* new fast code, 0.02ms per digestion */ - const arrayBuffer = imageData.data.buffer; + let data = imageData.data.slice(); + const BITS_PER_PIXEL = imageData.bitsPerPixel; + if (BITS_PER_PIXEL === 1 ) { + data = data.reduce((pixels, byte) => { + return pixels.concat(byte.toString(2).padStart(8, '0').split('').map(s => parseInt(s, 10))); + }, []); + } const result = []; let i = 0; - while (i < BYTE_LENGTH) { - if (result.length >= height) { - break; - } - - if (arrayBuffer.byteLength < (BYTE_OFFSET + i )) { + while (data.length > 0) { + if (result.length > height) { break; } - const row = new Uint1Array(arrayBuffer, i + BYTE_OFFSET, WIDTH_IN_BYTES); - result.push(row); - i += WIDTH_IN_BYTES; + result.push(data.splice(0, width)); + i += width; } return result; - // return result.reverse(); - - /* old slow code, ~2ms per digestion */ - // const gridarr = [...imageData.data]; - - // const grid2d = []; - // while(gridarr.length) grid2d.push(gridarr.splice(0, width)); - - // return grid2d; } /** @@ -103,58 +139,46 @@ function consumeImageData(imageData, width, height = Infinity) { function consumeRawGrids(raw) { const { mapSize, placementGrid: plGrid, pathingGrid: paGrid, terrainHeight: teGrid } = raw; const { x, y } = mapSize; - - // console.log(mapSize, plGrid, paGrid) - // @WIP: more trying to log / debug things... maybe helpful? const placementGrid2D = consumeImageData(plGrid, x, y); - debugGrid(placementGrid2D, raw.playableArea); + // debugGrid(placementGrid2D, raw.playableArea); - const pathingGrid2D = consumeImageData(paGrid, x, y); - + const pathingGrid2D = consumeImageData(paGrid, x, y).map(row => row.map(cell => cell === 0 ? 1 : 0)); + debugGrid(pathingGrid2D, raw.playableArea); const heightGrid2D = consumeImageData(teGrid, x, y); - + const height = heightGrid2D.map((row) => { return row.map(tile => { /** * functional approximation just isn't good enough here... * so unless we store a float, this is the other option - */ - const approx = Math.round((-100 + 200 * tile / 255)) * 10; - return Math.ceil(approx / 5) * 5; + const approx = Math.ceil( + Math.round(((tile - 127) / 8) * 10) / 5 + ) * 5; + if (approx < 0) return 0; + else return approx; }); }); - // const placement = placementGrid2D.map((row) => { - // return row.map(pixel => { - // return pixel === 255 ? 1 : 0; - // }); - // }); - - - - // const pathing = pathingGrid2D.map((row) => { - // return row.map(pixel => { - // return pixel === 255 ? 1 : 0; - // }); - // }); + const miniMap = placementGrid2D.map((row, y) => { + return row.map((pixel, x) => { + if (pixel === 1 && pathingGrid2D[y][x] === 1) { + return 66; + } else if (pixel === 0 && pathingGrid2D[y][x] === 0) { + return 114; + } else { + return pixel; + } + }); + }); return { height, + miniMap, placement: placementGrid2D, pathing: pathingGrid2D, - miniMap: placementGrid2D.map((row, y) => { - return row.map((pixel, x) => { - if (pixel === 1 && pathingGrid2D[y][x] === 1) { - return 66; - } else if (pixel === 0 && pathingGrid2D[y][x] === 0) { - return 114; - } else { - return pixel; - } - }); - }), }; } From 8d25195bd7d67a75273529cdebd24a891679268e Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 1 May 2020 19:00:31 -0400 Subject: [PATCH 04/24] updated: gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 393ad28..21fc365 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +test-bots/ + ### Node ### # Logs logs From 01642370068c3498f16c473fb98a4abff0906cde Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 1 May 2020 19:00:36 -0400 Subject: [PATCH 05/24] 0.9.0-alpha.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5400d0..1cedbe7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.8.2", + "version": "0.9.0-alpha.0", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From 0b25b53ad0f4930a66238982e398af7d1e2f6a84 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 1 May 2020 19:48:33 -0400 Subject: [PATCH 06/24] bump proto dep to new alpha version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cedbe7..237a04b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "eslint-config-problems": "^1.1.0" }, "dependencies": { - "@node-sc2/proto": "^0.5.7", + "@node-sc2/proto": "^0.6.0-alpha.0", "bluebird": "^3.5.3", "bresenham": "0.0.4", "chalk": "^3.0.0", From ea5eed6e77afb7c39a9660504394288f10ca1656 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 1 May 2020 19:48:37 -0400 Subject: [PATCH 07/24] 0.9.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 237a04b..807ffc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.0", + "version": "0.9.0-alpha.1", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From 26f0604d9c730790e91e010e5d12e201149018eb Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Mon, 4 May 2020 20:59:26 -0400 Subject: [PATCH 08/24] fixed floodfill algo (map and height were reversi) --- utils/map/flood.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/utils/map/flood.js b/utils/map/flood.js index d426be8..aba3839 100644 --- a/utils/map/flood.js +++ b/utils/map/flood.js @@ -14,8 +14,8 @@ const { distance } = require('../geometry/point'); */ function floodFill(mapData, x, y, oldVal, newVal, maxReach = false, filled = [], startPos) { - const mapWidth = mapData.length; - const mapHeight = mapData[0].length; + const mapHeight = mapData.length; + const mapWidth = mapData[0].length; if (startPos == null) { startPos = { x, y }; @@ -29,6 +29,7 @@ function floodFill(mapData, x, y, oldVal, newVal, maxReach = false, filled = [], return filled; } + // console.log(`in floodFill - x: ${x}, y: ${y}, maxReach: ${maxReach}`); if (mapData[y][x] !== oldVal || filled.some(pix => pix.x === x && pix.y === y)) { return filled; } From 23da784854b3ffd05a77f263dc3ccfee35824a3a Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Mon, 4 May 2020 21:00:03 -0400 Subject: [PATCH 09/24] fixed minimap caluclation --- utils/map/grid.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/utils/map/grid.js b/utils/map/grid.js index 7ca38f0..137ce4d 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -164,9 +164,9 @@ function consumeRawGrids(raw) { const miniMap = placementGrid2D.map((row, y) => { return row.map((pixel, x) => { - if (pixel === 1 && pathingGrid2D[y][x] === 1) { + if (pixel === 1 && pathingGrid2D[y][x] === 0) { return 66; - } else if (pixel === 0 && pathingGrid2D[y][x] === 0) { + } else if (pixel === 0 && pathingGrid2D[y][x] === 1) { return 114; } else { return pixel; @@ -174,6 +174,8 @@ function consumeRawGrids(raw) { }); }); + // debugGrid(miniMap, raw.playableArea); + return { height, miniMap, From b4bb1d39b8909efdb5e7b7966f607c760af21923 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Mon, 4 May 2020 21:02:25 -0400 Subject: [PATCH 10/24] 0.9.0-alpha.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 807ffc3..989c382 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.1", + "version": "0.9.0-alpha.2", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From 87f15fe3f25ea413c5c0db30fb0163458ed405c7 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Tue, 5 May 2020 11:47:55 -0400 Subject: [PATCH 11/24] refactoring some point utils --- utils/geometry/point.js | 54 ++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/utils/geometry/point.js b/utils/geometry/point.js index ebf472e..399ae54 100644 --- a/utils/geometry/point.js +++ b/utils/geometry/point.js @@ -179,35 +179,49 @@ const createPoint2D = ({x, y}) => ({ x: Math.floor(x), y: Math.floor(y) }); */ const createPoint = ({x, y, z}) => ({ x: Math.floor(x), y: Math.floor(y), z: Math.floor(z) }); + /** * - * @param {Point2D} point - * @param {boolean} includeDiagonal + * @param {Point2D} param0 + * @returns {Point2D[]} */ -function getNeighbors(point, includeDiagonal = true) { - const normal = createPoint2D(point); +const getAdjacents = ({ x, y }) => [ + { y: y - 1, x}, + { y, x: x - 1}, + { y, x: x + 1}, + { y: y + 1, x}, +]; - const getAdjacents = ({ x, y }) => [ - { y: y - 1, x}, - { y, x: x - 1}, - { y, x: x + 1}, - { y: y + 1, x}, - ]; +/** + * + * @param {Point2D} param0 + * @returns {Point2D[]} + */ +const getDiagonals = ({ x, y }) => [ + { y: y - 1, x: x - 1}, + { y: y - 1, x: x + 1}, + { y: y + 1, x: x - 1}, + { y: y + 1, x: x + 1}, +]; - const getDiags = ({ x, y }) => [ - { y: y - 1, x: x - 1}, - { y: y - 1, x: x + 1}, - { y: y + 1, x: x - 1}, - { y: y + 1, x: x + 1}, - ]; +/** + * + * @param {Point2D} point + * @param {boolean} includeDiagonals + * @param {boolean} diagonalsOnly + */ +function getNeighbors(point, includeDiagonals = true, diagonalsOnly = false) { + const normal = createPoint2D(point); let neighbors = getAdjacents(normal); - if (includeDiagonal) { - neighbors = neighbors.concat(getDiags(normal)); + if (includeDiagonals && diagonalsOnly) { + return getDiagonals(normal); + } else if (includeDiagonals) { + return neighbors.concat(getDiagonals(normal)); + } else { + return neighbors; } - - return neighbors; } module.exports = { From d76d41fe8a0f19fc243f4a4216ac98afadfc6df7 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Tue, 5 May 2020 11:48:43 -0400 Subject: [PATCH 12/24] tiny grid debug enhancement (and removed draw) --- utils/map/grid.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/utils/map/grid.js b/utils/map/grid.js index 137ce4d..55d39fa 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -31,10 +31,12 @@ function debugGrid(grid, playable, height = false) { case 66: return chalk.bgBlue` `; case 114: - return ' '; - case 104: // @ts-ignore - return chalk.bgGreen`░`; + return chalk.bgCyanBright`░`; + case 104: + + // ??? + return; case 72: // @ts-ignore return chalk.bgRed`░`; @@ -144,7 +146,7 @@ function consumeRawGrids(raw) { // debugGrid(placementGrid2D, raw.playableArea); const pathingGrid2D = consumeImageData(paGrid, x, y).map(row => row.map(cell => cell === 0 ? 1 : 0)); - debugGrid(pathingGrid2D, raw.playableArea); + // debugGrid(pathingGrid2D, raw.playableArea); const heightGrid2D = consumeImageData(teGrid, x, y); @@ -166,7 +168,7 @@ function consumeRawGrids(raw) { return row.map((pixel, x) => { if (pixel === 1 && pathingGrid2D[y][x] === 0) { return 66; - } else if (pixel === 0 && pathingGrid2D[y][x] === 1) { + } else if (pixel === 0 && pathingGrid2D[y][x] === 0) { return 114; } else { return pixel; From 3362de20f00aca6d581e984b2b0b27c1b143dea9 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Tue, 5 May 2020 12:09:26 -0400 Subject: [PATCH 13/24] ramp and wall calculation enhancement hopefully less brittle --- package.json | 1 + systems/map.js | 243 ++++++++++++++++++++++++++++++------------------- 2 files changed, 148 insertions(+), 96 deletions(-) diff --git a/package.json b/package.json index 989c382..c0ec6a5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@node-sc2/proto": "^0.6.0-alpha.0", + "array.prototype.flat": "^1.2.3", "bluebird": "^3.5.3", "bresenham": "0.0.4", "chalk": "^3.0.0", diff --git a/systems/map.js b/systems/map.js index b0a602d..354686f 100644 --- a/systems/map.js +++ b/systems/map.js @@ -5,6 +5,7 @@ const debugDrawMap = require('debug')('sc2:DrawDebugMap'); const debugDrawWalls = require('debug')('sc2:DrawDebugWalls'); const debugDebug = require('debug')('sc2:debug:MapSystem'); const silly = require('debug')('sc2:silly:MapSystem'); +const flat = require('array.prototype.flat'); const hullJs = require('hull.js'); const bresenham = require('bresenham'); const createSystem = require('./index'); @@ -13,7 +14,7 @@ const { createClusters: findClusters } = require('../utils/map/cluster'); const createExpansion = require('../engine/create-expansion'); const floodFill = require('../utils/map/flood'); const { distanceAAShapeAndPoint } = require('../utils/geometry/plane'); -const { distance, avgPoints, areEqual, closestPoint, createPoint2D} = require('../utils/geometry/point'); +const { distance, avgPoints, areEqual, closestPoint, createPoint2D, getNeighbors } = require('../utils/geometry/point'); const { frontOfGrid } = require('../utils/map/region'); const { gridsInCircle } = require('../utils/geometry/angle'); const { MineralField, VespeneGeyser, Townhall, getFootprint } = require('../utils/geometry/units'); @@ -134,110 +135,151 @@ function calculateRamps(minimap) { * @param {World} world */ function calculateWall(world, expansion) { - const { map, debug } = world.resources.get(); + const { map, units, debug } = world.resources.get(); + const { placement } = map.getGrids(); const hull = expansion.areas.hull; const foeHull = frontOfGrid(world, hull); // debug.setDrawCells('fonHull', foeHull.map(fh => ({ pos: fh })), { size: 0.50, color: Color.YELLOW, cube: true, persistText: true }); - const { pathing, miniMap } = map.getGrids(); - - const decomp = foeHull.reduce((decomp, { x, y }) => { - const neighbors = [ - { y: y - 1, x}, - { y, x: x - 1}, - { y, x: x + 1}, - { y: y + 1, x}, - ]; - - const diagNeighbors = [ - { y: y - 1, x: x - 1}, - { y: y - 1, x: x + 1}, - { y: y + 1, x: x - 1}, - { y: y + 1, x: x + 1}, - ]; - - const deadNeighbors = neighbors.filter(({ x, y }) => pathing[y][x] === 1); - const deadDiagNeighbors = diagNeighbors.filter(({ x, y }) => pathing[y][x] === 1); - - if ((deadNeighbors.length <= 0) && (deadDiagNeighbors.length <= 0)) { - // @FIXME: this is legitimately the worst possible way to check for a ramp - if (neighbors.filter(({ x, y }) => miniMap[y][x] === 114).length <= 0) { - decomp.liveHull.push({ x, y }); - } else { - decomp.liveRamp.push({ x, y }); - } - } - - decomp.deadHull = decomp.deadHull.concat(deadNeighbors); - return decomp; - }, { deadHull: [], liveHull: [], liveRamp: [] }); - const live = decomp.liveHull.length > 0 ? decomp.liveHull : decomp.liveRamp; + /** + * @FIXME: this is duplicated logic and can prolly be consolidated + */ - // debug.setDrawCells(`liveHull-${Math.floor(expansion.townhallPosition.x)}`, live.map(fh => ({ pos: fh })), { size: 0.5, color: Color.LIME_GREEN, cube: true }); - // debug.setDrawCells(`deadHull-${Math.floor(expansion.townhallPosition.x)}`, decomp.deadHull.map(fh => ({ pos: fh })), { size: 0.5, color: Color.RED, cube: true }); - const deadHullClusters = decomp.deadHull.reduce((clusters, dh) => { - if (clusters.length <= 0) { - const newCluster = [dh]; - newCluster.centroid = dh; - clusters.push(newCluster); + const plates = units.getByType(unbuildablePlateTypes); + const cellsBlockedByUnbuildableUnit = flat( + plates.map(plate => { + const footprint = getFootprint(plate.unitType); + return cellsInFootprint(createPoint2D(plate.pos), footprint); + }) + ); + // debug.setDrawCells('blockedCells', cellsBlockedByUnbuildableUnit.map(fh => ({ pos: fh })), { size: 0.50, color: Color.YELLOW, cube: true, persistText: true }); + + /** + * + * @param {Boolean} [rampDesired] + * @returns {Array} + */ + function findAllPossibleWalls(rampDesired = true) { + const rampTest = point => rampDesired ? map.isRamp(point) : !map.isRamp(point); + + const { deadHull, liveHull } = foeHull.reduce((decomp, point) => { + const neighbors = getNeighbors(point, false); + const diagNeighbors = getNeighbors(point, true, true); + + const deadNeighbors = neighbors.filter(point => !map.isPathable(point)); + const deadDiagNeighbors = diagNeighbors.filter(point => !map.isPathable(point)); + + if ((deadNeighbors.length <= 0) && (deadDiagNeighbors.length <= 0)) { + if (neighbors.some(rampTest)) { + if ( + (!neighbors.some(point => cellsBlockedByUnbuildableUnit.some(cell => areEqual(cell,point)))) + && (!diagNeighbors.some(point => cellsBlockedByUnbuildableUnit.some(cell => areEqual(cell,point)))) + ) { + decomp.liveHull.push(point); + } else { + // the ether... + } + } else { + // the ether... + } + } + + decomp.deadHull = decomp.deadHull.concat( + deadNeighbors.filter((neighborCell) => { + const neighborsNeighbors = getNeighbors(neighborCell); + return neighborsNeighbors.some(rampTest); + }) + ); + + return decomp; + }, { deadHull: [], liveHull: [] }); + + // debug.setDrawCells(`liveHull-${Math.floor(expansion.townhallPosition.x)}`, live.map(fh => ({ pos: fh })), { size: 0.75, color: Color.LIME_GREEN, cube: true }); + // debug.setDrawCells(`deadHull-${Math.floor(expansion.townhallPosition.x)}`, decomp.deadHull.map(fh => ({ pos: fh })), { size: 0.75, color: Color.RED, cube: true }); + + const deadHullClusters = deadHull.reduce((clusters, deadHullCell) => { + if (clusters.length <= 0) { + const newCluster = [deadHullCell]; + newCluster.centroid = deadHullCell; + clusters.push(newCluster); + return clusters; + } + + const clusterIndex = clusters.findIndex(cluster => distance(cluster.centroid, deadHullCell) < (liveHull.length - 1)); + if (clusterIndex !== -1) { + clusters[clusterIndex].push(deadHullCell); + clusters[clusterIndex].centroid = avgPoints(clusters[clusterIndex]); + } else { + const newCluster = [deadHullCell]; + newCluster.centroid = deadHullCell; + clusters.push(newCluster); + } + return clusters; - } - - const clusterIndex = clusters.findIndex(cluster => distance(cluster.centroid, dh) < (live.length - 1)); - if (clusterIndex !== -1) { - clusters[clusterIndex].push(dh); - clusters[clusterIndex].centroid = avgPoints(clusters[clusterIndex]); - } else { - const newCluster = [dh]; - newCluster.centroid = dh; - clusters.push(newCluster); - } - - return clusters; - }, []); + }, []); + + // debug.setDrawTextWorld(`liveHullLength-${Math.floor(expansion.townhallPosition.x)}`, [{ pos: createPoint2D(avgPoints(live)), text: `${live.length}` }]); + + // deadHullClusters.forEach((cluster, i) => { + // debug.setDrawCells(`dhcluster-${Math.floor(expansion.townhallPosition.x)}-${i}`, cluster.map(fh => ({ pos: fh })), { size: 0.8, cube: true }); + // }); + + return deadHullClusters.reduce((walls, cluster, i) => { + const possibleWalls = flat( + cluster.map(cell => { + const notOwnClusters = deadHullClusters.filter((c, j) => j !== i); + return notOwnClusters.map(jcluster => { + const closestCell = closestPoint(cell, jcluster); + const line = []; + bresenham(cell.x, cell.y, closestCell.x, closestCell.y, (x, y) => line.push({x, y})); + return line; + }); + }) + ); + + return walls.concat(possibleWalls); + }, []) + .map(wall => { + const first = wall[0]; + const last = wall[wall.length -1]; + + const newGraph = map.newGraph(placement.map(row => row.map(cell => cell === 0 ? 1 : 0))); + + newGraph.setWalkableAt(first.x, first.y, true); + newGraph.setWalkableAt(last.x, last.y, true); + return map.path(wall[0], wall[wall.length -1], { graph: newGraph, diagonal: true }) + .map(([x, y]) => ({ x, y })); + }) + .map(wall => { + // debug.setDrawCells(`middleWallCalc-${Math.floor(expansion.townhallPosition.x)}`, wall.map(fh => ({ pos: fh })), { size: 1, color: Color.HOT_PINK, cube: false }); + return wall.filter(cell => map.isPlaceable(cell)); + }) + .sort((a, b) => a.length - b.length) + .filter(wall => wall.length >= liveHull.length) + .filter (wall => distance(avgPoints(wall), avgPoints(liveHull)) <= liveHull.length) + } - // debug.setDrawTextWorld(`liveHullLength-${Math.floor(expansion.townhallPosition.x)}`, [{ pos: createPoint2D(avgPoints(live)), text: `${live.length}` }]); + // try first assuming we have a nat ramp + let allPossibleWalls = findAllPossibleWalls(true); - // deadHullClusters.forEach((cluster, i) => { - // debug.setDrawCells(`dhcluster-${Math.floor(expansion.townhallPosition.x)}-${i}`, cluster.map(fh => ({ pos: fh })), { size: 0.8, cube: true }); - // }); + if (!allPossibleWalls[0]) { + // now try assuming there is no ramp + allPossibleWalls = findAllPossibleWalls(false); + } - const allPossibleWalls = deadHullClusters.reduce((walls, cluster, i) => { - const possibleWalls = cluster.map(cell => { - const notOwnClusters = deadHullClusters.filter((c, j) => j !== i); - return notOwnClusters.map(jcluster => { - const closestCell = closestPoint(cell, jcluster); - const line = []; - bresenham(cell.x, cell.y, closestCell.x, closestCell.y, (x, y) => line.push({x, y})); - return line; - }); - }).reduce((walls, wall) => walls.concat(wall), []); - - return walls.concat(possibleWalls); - }, []) - .map(wall => { - const first = wall[0]; - const last = wall[wall.length -1]; - - const newGraph = map.newGraph(map._grids.pathing); - - newGraph.setWalkableAt(first.x, first.y, true); - newGraph.setWalkableAt(last.x, last.y, true); - return map.path(wall[0], wall[wall.length -1], { graph: newGraph, diagonal: true }) - .map(([x, y]) => ({ x, y })); - }) - .map(wall => wall.filter(cell => map.isPlaceable(cell))) - .sort((a, b) => a.length - b.length) - .filter(wall => wall.length >= live.length - 3) - .filter (wall => distance(avgPoints(wall), avgPoints(live)) <= live.length) - .filter((wall, i, arr) => wall.length === arr[0].length); - + /** + * @FIXME: we just sort of assume we always found a wall here... should be some contingency + */ const [shortestWall] = allPossibleWalls; - + if (debugDrawWalls.enabled) { - debug.setDrawCells(`dhwall`, shortestWall.map(fh => ({ pos: fh })), { size: 0.8, color: Color.YELLOW, cube: true, persistText: true, }); + debug.setDrawCells( + `dhwall`, + shortestWall.map(fh => ({ pos: fh })), + { size: 0.9, color: Color.FUCHSIA, cube: true, persistText: true } + ); } expansion.areas.wall = shortestWall; @@ -334,6 +376,19 @@ const mapSystem = { map.setRamps(calculateRamps(map._grids.miniMap).map((rPoint) => { return Object.assign(rPoint, { z: map.getHeight(rPoint) }); })); + + map.setRamps(map._ramps.filter((possibleRamp) => { + const { x, y } = possibleRamp; + // this is necessary because some maps have random tiles that aren't placeable but are pathable + const neighbors = [ + { y: y - 1, x}, + { y, x: x - 1}, + { y, x: x + 1}, + { y: y + 1, x}, + ]; + + return neighbors.some(point => map.isRamp(point)); + })); // mark cells under geysers, destructable plates, and mineral fields as initially unplaceable const geysers = units.getGasGeysers(); @@ -361,10 +416,6 @@ const mapSystem = { // debug.setDrawCells('ramps', map._ramps.map(r => ({ pos: r })), { size: 0.5, cube: true }); calculateExpansions(world); - - /** - * i guess... don't uncomment this until it's fixed? - */ calculateWall(world, map.getNatural()); }, async onStep({ data, resources }) { From 1d661a3c6a3458c43e6bc4336f330af97f83b97c Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Tue, 5 May 2020 12:09:42 -0400 Subject: [PATCH 14/24] 0.9.0-alpha.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c0ec6a5..0df8054 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.2", + "version": "0.9.0-alpha.3", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From 962801a0c48ad69ec0230389fc075130f245cc63 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Thu, 28 May 2020 16:02:55 -0400 Subject: [PATCH 15/24] removing uint1array require --- utils/map/grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/map/grid.js b/utils/map/grid.js index 55d39fa..336a310 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -1,7 +1,7 @@ 'use strict'; const chalk = require('chalk'); -const Uint1Array = require('uint1array').default; +// const Uint1Array = require('uint1array').default; /** * @param {Grid2D} grid From fa5143dd7ca9a178e167e7637e56f7c73fb8adb5 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Thu, 28 May 2020 16:05:49 -0400 Subject: [PATCH 16/24] 0.9.0-alpha.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0df8054..9b8ef06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.3", + "version": "0.9.0-alpha.4", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From 7aae3443ce590424aa02660cb74e6ccd01fc58a4 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Wed, 10 Jun 2020 17:22:26 -0400 Subject: [PATCH 17/24] zerg structure cost fix - inject logic fix --- agent/create-agent.js | 3 +++ engine/create-unit.js | 14 ++++++++++---- engine/data-storage.js | 16 +++++++++++++++- interfaces.d.ts | 2 +- resources/units.js | 6 ++++-- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/agent/create-agent.js b/agent/create-agent.js index 8fe2e9d..6e498f5 100644 --- a/agent/create-agent.js +++ b/agent/create-agent.js @@ -43,6 +43,9 @@ function createAgent(blueprint = {}) { const earmarks = data.getEarmarkTotals(earmarkName); const unitType = data.getUnitTypeData(unitTypeId); + + // console.log("current earmarks", earmarks); + // console.log("mineral cost:", unitType.mineralCost); const result = ( (minerals - earmarks.minerals >= unitType.mineralCost) && diff --git a/engine/create-unit.js b/engine/create-unit.js index d15b0fe..0af94c9 100644 --- a/engine/create-unit.js +++ b/engine/create-unit.js @@ -2,7 +2,8 @@ const UnitType = require("../constants/unit-type"); const Ability = require("../constants/ability"); -const { Alliance, WeaponTargetType, Attribute, CloakState } = require("../constants/enums"); +const { TownhallRace } = require("../constants/race-map"); +const { Alliance, WeaponTargetType, Attribute, CloakState, Race } = require("../constants/enums"); const { techLabTypes, reactorTypes, @@ -39,9 +40,14 @@ function createUnit(unitData, { data, resources }) { if (t) { target = t; } else { - const [idleHatch] = units.getById(UnitType.HATCH).filter(u => u.isIdle()); - if (idleHatch) { - target = idleHatch; + const [closestIdleHatch] = units.getClosest( + this.pos, + units.getById(TownhallRace[Race.ZERG]), + 3 + ).filter(u => u.isIdle()); + + if (closestIdleHatch) { + target = closestIdleHatch; } else { return; } diff --git a/engine/data-storage.js b/engine/data-storage.js index da24230..d0ac714 100644 --- a/engine/data-storage.js +++ b/engine/data-storage.js @@ -1,6 +1,7 @@ "use strict"; const debugEarmark = require('debug')('sc2:debug:earmark'); +const { Race, Attribute } = require('../constants/enums'); const { AbilitiesByUnit } = require('../constants'); /** @@ -29,7 +30,20 @@ function createDataManager() { .map(unitAbility => parseInt(unitAbility[0], 10)); }, getUnitTypeData(unitTypeId) { - return this.get('units')[unitTypeId]; + /** @type {SC2APIProtocol.UnitTypeData} */ + const unitData = this.get('units')[unitTypeId]; + + /** + * Fixes unit cost for zerg structures (removes the 'drone cost' inflation) + */ + if (unitData.race === Race.ZERG && unitData.attributes.includes(Attribute.STRUCTURE)) { + return { + ...unitData, + mineralCost: unitData.mineralCost - 50, + }; + } else { + return unitData; + } }, getUpgradeData(upgradeId) { return this.get('upgrades')[upgradeId]; diff --git a/interfaces.d.ts b/interfaces.d.ts index 9203c15..86f137c 100644 --- a/interfaces.d.ts +++ b/interfaces.d.ts @@ -185,7 +185,7 @@ interface UnitResource { getRangedCombatUnits(): Unit[]; getAll: (filter?: (number | UnitFilter)) => Unit[]; getAlive: (filter?: (number | UnitFilter)) => Unit[]; - getById: (unitTypeId: number, filter?: UnitFilter) => Unit[]; + getById: (unitTypeId: (number | number[]), filter?: UnitFilter) => Unit[]; getByTag(unitTags: string): Unit; getByTag(unitTags: string[]): Unit[]; getClosest(pos: Point2D, units: Unit[], n?: number): Unit[]; diff --git a/resources/units.js b/resources/units.js index 2e303f9..c5497a3 100644 --- a/resources/units.js +++ b/resources/units.js @@ -111,8 +111,10 @@ function createUnits(world) { getAlive(filter) { return this.getAll(filter).filter(u => u.isCurrent()); }, - getById(unitTypeId, filter = { alliance: Alliance.SELF }) { - return this.getAlive(filter).filter(u => u.unitType === unitTypeId); + getById(unitTypeIds, filter = { alliance: Alliance.SELF }) { + const typeIds = Array.isArray(unitTypeIds) ? unitTypeIds : [unitTypeIds]; + + return this.getAlive(filter).filter(u => typeIds.includes(u.unitType)); }, // @ts-ignore overloads are hard apparently getByTag(unitTags) { From 417adc44c0127e89d95f1b799890dd92a22d7055 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Wed, 10 Jun 2020 17:24:30 -0400 Subject: [PATCH 18/24] 0.9.0-alpha.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b8ef06..94a0b97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.4", + "version": "0.9.0-alpha.5", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From 9d8da026180d2f6fcb1174c87456a8f331906416 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Mon, 15 Jun 2020 14:43:13 -0400 Subject: [PATCH 19/24] fix to work again in ladder managed games --- agent/create-agent.js | 2 +- engine/create-engine.js | 1 + interfaces.d.ts | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/agent/create-agent.js b/agent/create-agent.js index 6e498f5..b61fff8 100644 --- a/agent/create-agent.js +++ b/agent/create-agent.js @@ -188,7 +188,7 @@ function createAgent(blueprint = {}) { * @TODO: the first time we see an enemy unit, we should set the value of this on the agent and then * memoize it */ - this.enemy = { + this.opponent = { race: enemyPlayer.raceRequested !== Race.RANDOM ? enemyPlayer.raceRequested : Race.NORACE, }; } diff --git a/engine/create-engine.js b/engine/create-engine.js index f716a68..ce03536 100644 --- a/engine/create-engine.js +++ b/engine/create-engine.js @@ -212,6 +212,7 @@ function createEngine(options = {}) { const { data, resources, agent } = world; if (isManaged) { + agent.opponent = agent.opponent || {}; agent.opponent.id = argv.OpponentId; } diff --git a/interfaces.d.ts b/interfaces.d.ts index 86f137c..8f98467 100644 --- a/interfaces.d.ts +++ b/interfaces.d.ts @@ -105,8 +105,8 @@ interface SystemWrapper { } type Opponent = { - id: string; - race: SC2APIProtocol.Race; + id?: string; + race?: SC2APIProtocol.Race; } interface PlayerData extends SC2APIProtocol.PlayerCommon, SC2APIProtocol.PlayerRaw { } From 9a39adf669293223276de02476d5f275f6062289 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Mon, 15 Jun 2020 14:43:26 -0400 Subject: [PATCH 20/24] 0.9.0-alpha.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 94a0b97..1cc52f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.5", + "version": "0.9.0-alpha.6", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From 537700725d05282e233f7047cc43724f745f57d5 Mon Sep 17 00:00:00 2001 From: "Daniel-PC\\Daniel" Date: Tue, 29 Sep 2020 22:05:29 -0400 Subject: [PATCH 21/24] Fix to avoid initializing file paths until after the function is called. --- engine/launcher.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/engine/launcher.js b/engine/launcher.js index e0e8c3a..09445c8 100644 --- a/engine/launcher.js +++ b/engine/launcher.js @@ -9,24 +9,13 @@ const path = require('path'); const findP = require('find-process'); let EXECUTE_INFO_PATH; -if (os.platform() === 'darwin') { - EXECUTE_INFO_PATH = path.join('Library', 'Application Support', 'Blizzard', 'StarCraft II', 'ExecuteInfo.txt'); -} else { - EXECUTE_INFO_PATH = path.join('Documents', 'StarCraft II', 'ExecuteInfo.txt'); -} - -const HOME_DIR = os.homedir(); - -const executeInfoText = fs.readFileSync(path.join(HOME_DIR, EXECUTE_INFO_PATH)).toString(); -const executablePath = executeInfoText.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/m)[2]; - -const parsedPath = executablePath.split(path.sep); -const execName = parsedPath[parsedPath.length - 1]; - -const basePath = parsedPath.slice(0, parsedPath.findIndex(s => s === 'StarCraft II') + 1).join(path.sep); +let executablePath; +let execName; +let basePath; /** @type {Launcher} */ async function launcher(options = {}) { + setupFilePaths(); const opts = { listen: '127.0.0.1', port: 5000, @@ -119,4 +108,22 @@ async function findMap(mapName) { throw new Error(`Map "${mapName}" not found`); } +function setupFilePaths() { + if (os.platform() === 'darwin') { + EXECUTE_INFO_PATH = path.join('Library', 'Application Support', 'Blizzard', 'StarCraft II', 'ExecuteInfo.txt'); + } else { + EXECUTE_INFO_PATH = path.join('Documents', 'StarCraft II', 'ExecuteInfo.txt'); + } + + const HOME_DIR = os.homedir(); + + const executeInfoText = fs.readFileSync(path.join(HOME_DIR, EXECUTE_INFO_PATH)).toString(); + executablePath = executeInfoText.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/m)[2]; + + const parsedPath = executablePath.split(path.sep); + execName = parsedPath[parsedPath.length - 1]; + + basePath = parsedPath.slice(0, parsedPath.findIndex(s => s === 'StarCraft II') + 1).join(path.sep); +} + module.exports = { launcher, findMap }; From b306232c08288e0e5b89f8b5fba7e98123a442eb Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 25 Dec 2020 12:53:24 -0500 Subject: [PATCH 22/24] optimize bit conversion from 1 bit pixel grids --- utils/map/grid.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/utils/map/grid.js b/utils/map/grid.js index 336a310..224a85a 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -115,11 +115,22 @@ function consumeImageData(imageData, width, height = Infinity) { if (BITS_PER_PIXEL === 1 ) { data = data.reduce((pixels, byte) => { - return pixels.concat(byte.toString(2).padStart(8, '0').split('').map(s => parseInt(s, 10))); + pixels.push( + (byte >> 7) & 1, + (byte >> 6) & 1, + (byte >> 5) & 1, + (byte >> 4) & 1, + (byte >> 3) & 1, + (byte >> 2) & 1, + (byte >> 1) & 1, + (byte >> 0) & 1 + ); + + return pixels; }, []); } + const result = []; - let i = 0; while (data.length > 0) { if (result.length > height) { @@ -127,7 +138,6 @@ function consumeImageData(imageData, width, height = Infinity) { } result.push(data.splice(0, width)); - i += width; } return result; From 28754815e1422c3d2cfcd8aa5ec570994f5ef7f9 Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 25 Dec 2020 12:55:17 -0500 Subject: [PATCH 23/24] 0.9.0-alpha.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cc52f8..bd79c6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.6", + "version": "0.9.0-alpha.7", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", From a2ca17f16f94a3bc481e9908894933e0e548113a Mon Sep 17 00:00:00 2001 From: Jared Kantrowitz Date: Fri, 25 Dec 2020 13:03:05 -0500 Subject: [PATCH 24/24] 0.9.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd79c6c..dfc707a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.9.0-alpha.7", + "version": "0.9.0-beta.1", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts",