commit 3cd6a07fbf33b01aaeca7c301e15cd0ae516fdbe Author: Anton Vakhrushev Date: Sun May 31 15:06:55 2020 +0300 Init files diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a5f82df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +indent_style = space +indent_size = 4 + +[*.rb] +indent_style = space +indent_size = 4 diff --git a/.env b/.env new file mode 100644 index 0000000..1d60765 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NODE_IMAGE=screeps-node diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce84fb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,92 @@ +# yarn lock file +/yarn.lock + +# npm lock file (v5.0.0+) +/package-lock.json + +# Ignore basic folders +/dist +/.rpt2_cache +/tsc-out +/node_modules +/_book +/build/* + +# TypeScript definitions installed by Typings +/typings + +# Screeps Config +screeps.json + +# ScreepsServer data from integration tests +/server + +# Numerous always-ignore extensions +*.diff +*.err +*.orig +*.log +*.rej +*.swo +*.swp +*.zip +*.vi +*~ + +# Editor folders +.cache +.project +.settings +.tmproj +*.esproj +nbproject +*.sublime-project +*.sublime-workspace +.idea + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2580248 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "tabWidth": 2, + "printWidth": 120, + "singleQuote": true, + "trailingComma": "none" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3203f7b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM node:10-alpine + +RUN apk add --no-cache --virtual .gyp \ + python \ + make \ + g++ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..63092af --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +all: format build + +build-docker: + docker build -t screeps-node . + +build: + tools/npm run build:dev + +format: + tools/npm run format + +test: + tools/npm run test + +coverage: + tools/npm run coverage diff --git a/README.md b/README.md new file mode 100644 index 0000000..04591aa --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Screeps Typescript Starter + +Screeps Typescript Starter is a starting point for a Screeps AI written in Typescript. It provides everything you need to start writing your AI whilst leaving `main.ts` as empty as possible. + +## Basic Usage + +You will need: + +- [Node.JS](https://nodejs.org/en/download) (10.x) +- A Package Manager ([Yarn](https://yarnpkg.com/en/docs/getting-started) or [npm](https://docs.npmjs.com/getting-started/installing-node)) +- Rollup CLI (Optional, install via `npm install -g rollup`) + +Download the latest source [here](https://github.com/screepers/screeps-typescript-starter/archive/master.zip) and extract it to a folder. + +Open the folder in your terminal and run your package manager to install install the required packages and TypeScript declaration files: + +```bash +# npm +npm install + +# yarn +yarn +``` + +Fire up your preferred editor with typescript installed and you are good to go! + +### Rollup and code upload + +Screeps Typescript Starter uses rollup to compile your typescript and upload it to a screeps server. + +Move or copy `screeps.sample.json` to `screeps.json` and edit it, changing the credentials and optionally adding or removing some of the destinations. + +Running `rollup -c` will compile your code and do a "dry run", preparing the code for upload but not actually pushing it. Running `rollup -c --environment DEST:main` will compile your code, and then upload it to a screeps server using the `main` config from `screeps.json`. + +You can use `-cw` instead of `-c` to automatically re-run when your source code changes - for example, `rollup -cw --environment DEST:main` will automatically upload your code to the `main` configuration every time your code is changed. + +Finally, there are also NPM scripts that serve as aliases for these commands in `package.json` for IDE integration. Running `npm run push-main` is equivalent to `rollup -c --environment DEST:main`, and `npm run watch-sim` is equivalent to `rollup -cw --dest sim`. + +#### Important! To upload code to a private server, you must have [screepsmod-auth](https://github.com/ScreepsMods/screepsmod-auth) installed and configured! + +## Typings + +The type definitions for Screeps come from [typed-screeps](https://github.com/screepers/typed-screeps). If you find a problem or have a suggestion, please open an issue there. + +## Documentation + +We've also spent some time reworking the documentation from the ground-up, which is now generated through [Gitbooks](https://www.gitbook.com/). Includes all the essentials to get you up and running with Screeps AI development in TypeScript, as well as various other tips and tricks to further improve your development workflow. + +Maintaining the docs will also become a more community-focused effort, which means you too, can take part in improving the docs for this starter kit. + +To visit the docs, [click here](https://screepers.gitbook.io/screeps-typescript-starter/). + +## Contributing + +Issues, Pull Requests, and contribution to the docs are welcome! See our [Contributing Guidelines](CONTRIBUTING.md) for more details. diff --git a/package.json b/package.json new file mode 100644 index 0000000..befa175 --- /dev/null +++ b/package.json @@ -0,0 +1,66 @@ +{ + "name": "screeps-typescript-starter", + "version": "3.0.0", + "description": "", + "main": "index.js", + "//": "If you add or change the names of destinations in screeps.json, make sure you update these scripts to reflect the changes", + "scripts": { + "lint": "tslint -p tsconfig.json \"src/**/*.ts\"", + "build": "rollup -c", + "push-main": "rollup -c --environment DEST:main", + "push-pserver": "rollup -c --environment DEST:pserver", + "push-sim": "rollup -c --environment DEST:sim", + "format": "prettier --write \"src/**/*.ts\"", + "test": "npm run test-unit", + "test-unit": "rollup -c rollup.test-unit-config.js && mocha dist/test-unit.bundle.js", + "test-integration": "echo 'See docs/in-depth/testing.md for instructions on enabling integration tests'", + "watch-main": "rollup -cw --environment DEST:main", + "watch-pserver": "rollup -cw --environment DEST:pserver", + "watch-sim": "rollup -cw --environment DEST:sim" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/screepers/screeps-typescript-starter.git" + }, + "author": "", + "license": "Unlicense", + "bugs": { + "url": "https://github.com/screepers/screeps-typescript-starter/issues" + }, + "homepage": "https://github.com/screepers/screeps-typescript-starter#readme", + "engines": { + "node": "10.x" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^11.1.0", + "@rollup/plugin-multi-entry": "^3.0.0", + "@rollup/plugin-node-resolve": "^7.1.3", + "@types/chai": "^4.1.6", + "@types/lodash": "3.10.2", + "@types/mocha": "^5.2.5", + "@types/node": "^13.13.1", + "@types/screeps": "^3.1.0", + "@types/sinon": "^5.0.5", + "@types/sinon-chai": "^3.2.0", + "chai": "^4.2.0", + "lodash": "^3.10.1", + "mocha": "^5.2.0", + "prettier": "^2.0.4", + "rollup": "^2.6.1", + "rollup-plugin-buble": "^0.19.8", + "rollup-plugin-clear": "^2.0.7", + "rollup-plugin-nodent": "^0.2.2", + "rollup-plugin-screeps": "^1.0.0", + "rollup-plugin-typescript2": "^0.27.0", + "sinon": "^6.3.5", + "sinon-chai": "^3.2.0", + "ts-node": "^8.8.2", + "tslint": "^6.1.1", + "tslint-config-prettier": "^1.18.0", + "tslint-plugin-prettier": "^2.3.0", + "typescript": "^3.8.3" + }, + "dependencies": { + "source-map": "~0.6.1" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..b0ea090 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,32 @@ +"use strict"; + +import clear from 'rollup-plugin-clear'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from 'rollup-plugin-typescript2'; +import screeps from 'rollup-plugin-screeps'; + +let cfg; +const dest = process.env.DEST; +if (!dest) { + console.log("No destination specified - code will be compiled but not uploaded"); +} else if ((cfg = require("./screeps.json")[dest]) == null) { + throw new Error("Invalid upload destination"); +} + +export default { + input: "src/main.ts", + output: { + file: "dist/main.js", + format: "cjs", + sourcemap: true + }, + + plugins: [ + clear({ targets: ["dist"] }), + resolve(), + commonjs(), + typescript({tsconfig: "./tsconfig.json"}), + screeps({config: cfg, dryRun: cfg == null}) + ] +} diff --git a/rollup.test-integration-config.js b/rollup.test-integration-config.js new file mode 100644 index 0000000..5493f10 --- /dev/null +++ b/rollup.test-integration-config.js @@ -0,0 +1,34 @@ +"use strict"; + +import clear from 'rollup-plugin-clear'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from 'rollup-plugin-typescript2'; +import buble from 'rollup-plugin-buble'; +import multiEntry from '@rollup/plugin-multi-entry'; +import nodent from 'rollup-plugin-nodent'; + +export default { + input: 'test/integration/**/*.test.ts', + output: { + file: 'dist/test-integration.bundle.js', + name: 'lib', + sourcemap: true, + format: 'iife', + globals: { + chai: 'chai', + it: 'it', + describe: 'describe' + } + }, + external: ['chai', 'it', 'describe'], + plugins: [ + clear({ targets: ["dist/test.bundle.js"] }), + resolve(), + commonjs(), + typescript({tsconfig: "./tsconfig.test-integration.json"}), + nodent(), + multiEntry(), + buble() + ] +} diff --git a/rollup.test-unit-config.js b/rollup.test-unit-config.js new file mode 100644 index 0000000..4cea680 --- /dev/null +++ b/rollup.test-unit-config.js @@ -0,0 +1,32 @@ +"use strict"; + +import clear from 'rollup-plugin-clear'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from 'rollup-plugin-typescript2'; +import buble from 'rollup-plugin-buble'; +import multiEntry from '@rollup/plugin-multi-entry'; + +export default { + input: 'test/unit/**/*.test.ts', + output: { + file: 'dist/test-unit.bundle.js', + name: 'lib', + sourcemap: true, + format: 'iife', + globals: { + chai: 'chai', + it: 'it', + describe: 'describe' + } + }, + external: ['chai', 'it', 'describe'], + plugins: [ + clear({ targets: ["dist/test.bundle.js"] }), + resolve(), + commonjs(), + typescript({tsconfig: "./tsconfig.json"}), + multiEntry(), + buble() + ] +} diff --git a/screeps.sample.json b/screeps.sample.json new file mode 100644 index 0000000..14bff62 --- /dev/null +++ b/screeps.sample.json @@ -0,0 +1,27 @@ +{ + "main": { + "token": "YOUR_TOKEN", + "protocol": "https", + "hostname": "screeps.com", + "port": 443, + "path": "/", + "branch": "main" + }, + "sim": { + "token": "YOUR_TOKEN", + "protocol": "https", + "hostname": "screeps.com", + "port": 443, + "path": "/", + "branch": "sim" + }, + "pserver": { + "email": "username", + "password": "Password", + "protocol": "http", + "hostname": "1.2.3.4", + "port": 21025, + "path": "/", + "branch": "main" + } +} diff --git a/src/harvester.ts b/src/harvester.ts new file mode 100644 index 0000000..8567a62 --- /dev/null +++ b/src/harvester.ts @@ -0,0 +1,93 @@ +/** @param {Creep} creep **/ +export function runAsHarvester(creep: Creep) { + if (creep.store.getFreeCapacity() > 0) { + let sources = creep.room.find(FIND_SOURCES); + if (creep.harvest(sources[0]) === ERR_NOT_IN_RANGE) { + creep.moveTo(sources[0], { visualizePathStyle: { stroke: '#ffaa00' } }); + } + } else { + let targets = creep.room.find(FIND_STRUCTURES, { + filter: (structure) => { + return ( + (structure.structureType === STRUCTURE_EXTENSION || + structure.structureType === STRUCTURE_SPAWN || + structure.structureType === STRUCTURE_TOWER) && + structure.store.getFreeCapacity(RESOURCE_ENERGY) > 0 + ); + } + }); + + if (targets.length > 0) { + if (creep.transfer(targets[0], RESOURCE_ENERGY) === ERR_NOT_IN_RANGE) { + creep.moveTo(targets[0], { visualizePathStyle: { stroke: '#ffffff' } }); + } + } else { + const spawns = creep.room.find(FIND_MY_SPAWNS); + if (spawns.length > 0) { + creep.say('to spawn'); + creep.moveTo(spawns[0]); + } + } + } +} + +/** @param {Creep} creep **/ +export function runAsBuilder(creep: Creep) { + const memory = creep.memory as CreepMemory & { building: boolean | undefined }; + + if (memory.building && creep.store[RESOURCE_ENERGY] === 0) { + memory.building = false; + creep.say('🔄 harvest'); + } + + if (!memory.building && creep.store.getFreeCapacity() === 0) { + memory.building = true; + creep.say('🚧 build'); + } + + if (memory.building) { + const targets = creep.room.find(FIND_CONSTRUCTION_SITES); + if (targets.length > 0) { + if (creep.build(targets[0]) == ERR_NOT_IN_RANGE) { + creep.moveTo(targets[0], { visualizePathStyle: { stroke: '#ffffff' } }); + } + } else { + const spawns = creep.room.find(FIND_MY_SPAWNS); + if (spawns.length > 0) { + creep.say('to spawn'); + creep.moveTo(spawns[0]); + } + } + } else { + const sources = creep.room.find(FIND_SOURCES); + if (creep.harvest(sources[0]) == ERR_NOT_IN_RANGE) { + creep.moveTo(sources[0], { visualizePathStyle: { stroke: '#ffaa00' } }); + } + } +} + +/** @param {Creep} creep **/ +export function runAsUpgrader(creep: Creep) { + const memory = creep.memory as CreepMemory & { upgrading: boolean | undefined }; + + if (memory.upgrading && creep.store[RESOURCE_ENERGY] === 0) { + memory.upgrading = false; + creep.say('🔄 harvest'); + } + + if (!memory.upgrading && creep.store.getFreeCapacity() === 0) { + memory.upgrading = true; + creep.say('⚡ upgrade'); + } + + if (memory.upgrading) { + if (creep.room.controller && creep.upgradeController(creep.room.controller) == ERR_NOT_IN_RANGE) { + creep.moveTo(creep.room.controller, { visualizePathStyle: { stroke: '#ffffff' } }); + } + } else { + const sources = creep.room.find(FIND_SOURCES); + if (creep.harvest(sources[0]) == ERR_NOT_IN_RANGE) { + creep.moveTo(sources[0], { visualizePathStyle: { stroke: '#ffaa00' } }); + } + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a9116dd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,77 @@ +import { ErrorMapper } from 'utils/ErrorMapper'; +import { runAsBuilder, runAsHarvester, runAsUpgrader } from './harvester'; + +const ALPHABET = 'abcdefghijklmnopqrstuvwxyz1234567890'; +const ALPHABET_LENGTH = ALPHABET.length - 1; + +function generateId(count: number): string { + let str = ''; + for (let i = 0; i < count; ++i) { + let symbolIndex = Math.floor(Math.random() * ALPHABET_LENGTH); + str += ALPHABET[symbolIndex]; + } + return str; +} + +export function uniqId(prefix: string = 'id'): string { + return prefix + generateId(16); +} + +const ROLE_HARVESTER = 'harvester'; +const ROLE_UPGRADER = 'upgrader'; +const ROLE_BUILDER = 'builder'; + +// When compiling TS to JS and bundling with rollup, the line numbers and file names in error messages change +// This utility uses source maps to get the line numbers and file names of the original, TS source code +export const loop = ErrorMapper.wrapLoop(() => { + console.log(`Current game tick is ${Game.time}`); + + // Automatically delete memory of missing creeps + for (const name in Memory.creeps) { + if (!(name in Game.creeps)) { + delete Memory.creeps[name]; + } + } + + // Create new creeps + const HARVESTER_CREEP_COUNT = 1; + const harvesterCreeps = Object.values(Game.creeps).filter((c) => c.memory.role === ROLE_HARVESTER); + if (harvesterCreeps.length < HARVESTER_CREEP_COUNT) { + const firstSpawn = _.first(Object.values(Game.spawns)); + const name = uniqId(ROLE_HARVESTER); + const err = firstSpawn.spawnCreep([WORK, CARRY, MOVE], name, { memory: { role: ROLE_HARVESTER } as CreepMemory }); + console.log('Err', err); + } + + const UPGRADER_CREEP_COUNT = 2; + const upgraderCreeps = Object.values(Game.creeps).filter((c) => c.memory.role === ROLE_UPGRADER); + if (upgraderCreeps.length < UPGRADER_CREEP_COUNT) { + const firstSpawn = _.first(Object.values(Game.spawns)); + const name = uniqId(ROLE_UPGRADER); + const err = firstSpawn.spawnCreep([WORK, CARRY, MOVE], name, { memory: { role: ROLE_UPGRADER } as CreepMemory }); + console.log('Err', err); + } + + const BUILDER_CREEP_COUNT = 2; + const builderCreeps = Object.values(Game.creeps).filter((c) => c.memory.role === ROLE_BUILDER); + if (builderCreeps.length < BUILDER_CREEP_COUNT) { + const firstSpawn = _.first(Object.values(Game.spawns)); + const name = uniqId(ROLE_BUILDER); + const err = firstSpawn.spawnCreep([WORK, CARRY, MOVE], name, { memory: { role: ROLE_BUILDER } as CreepMemory }); + console.log('Err', err); + } + + // Process current creeps + for (let name in Game.creeps) { + const creep = Game.creeps[name]; + if (creep.memory.role === ROLE_HARVESTER) { + runAsHarvester(creep); + } + if (creep.memory.role === ROLE_UPGRADER) { + runAsUpgrader(creep); + } + if (creep.memory.role === ROLE_BUILDER) { + runAsBuilder(creep); + } + } +}); diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..00727b9 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,20 @@ +// example declaration file - remove these and add your own custom typings + +// memory extension samples +interface CreepMemory { + role: string; + room: string; + working: boolean; +} + +interface Memory { + uuid: number; + log: any; +} + +// `global` extension samples +declare namespace NodeJS { + interface Global { + log: any; + } +} diff --git a/src/utils/ErrorMapper.ts b/src/utils/ErrorMapper.ts new file mode 100644 index 0000000..da85177 --- /dev/null +++ b/src/utils/ErrorMapper.ts @@ -0,0 +1,90 @@ +// tslint:disable:no-conditional-assignment +import { SourceMapConsumer } from 'source-map'; + +export class ErrorMapper { + // Cache consumer + private static _consumer?: SourceMapConsumer; + + public static get consumer(): SourceMapConsumer { + if (this._consumer == null) { + this._consumer = new SourceMapConsumer(require('main.js.map')); + } + + return this._consumer; + } + + // Cache previously mapped traces to improve performance + public static cache: { [key: string]: string } = {}; + + /** + * Generates a stack trace using a source map generate original symbol names. + * + * WARNING - EXTREMELY high CPU cost for first call after reset - >30 CPU! Use sparingly! + * (Consecutive calls after a reset are more reasonable, ~0.1 CPU/ea) + * + * @param {Error | string} error The error or original stack trace + * @returns {string} The source-mapped stack trace + */ + public static sourceMappedStackTrace(error: Error | string): string { + const stack: string = error instanceof Error ? (error.stack as string) : error; + if (this.cache.hasOwnProperty(stack)) { + return this.cache[stack]; + } + + const re = /^\s+at\s+(.+?\s+)?\(?([0-z._\-\\\/]+):(\d+):(\d+)\)?$/gm; + let match: RegExpExecArray | null; + let outStack = error.toString(); + + while ((match = re.exec(stack))) { + if (match[2] === 'main') { + const pos = this.consumer.originalPositionFor({ + column: parseInt(match[4], 10), + line: parseInt(match[3], 10) + }); + + if (pos.line != null) { + if (pos.name) { + outStack += `\n at ${pos.name} (${pos.source}:${pos.line}:${pos.column})`; + } else { + if (match[1]) { + // no original source file name known - use file name from given trace + outStack += `\n at ${match[1]} (${pos.source}:${pos.line}:${pos.column})`; + } else { + // no original source file name known or in given trace - omit name + outStack += `\n at ${pos.source}:${pos.line}:${pos.column}`; + } + } + } else { + // no known position + break; + } + } else { + // no more parseable lines + break; + } + } + + this.cache[stack] = outStack; + return outStack; + } + + public static wrapLoop(loop: () => void): () => void { + return () => { + try { + loop(); + } catch (e) { + if (e instanceof Error) { + if ('sim' in Game.rooms) { + const message = `Source maps don't work in the simulator - displaying original error`; + console.log(`${message}
${_.escape(e.stack)}
`); + } else { + console.log(`${_.escape(this.sourceMappedStackTrace(e))}`); + } + } else { + // can't handle it + throw e; + } + } + }; + } +} diff --git a/test/integration/helper.ts b/test/integration/helper.ts new file mode 100644 index 0000000..18bc749 --- /dev/null +++ b/test/integration/helper.ts @@ -0,0 +1,59 @@ +const { readFileSync } = require('fs'); +const _ = require('lodash'); +const { ScreepsServer, stdHooks } = require('screeps-server-mockup'); +const DIST_MAIN_JS = 'dist/main.js'; + +/* + * Helper class for creating a ScreepsServer and resetting it between tests. + * See https://github.com/Hiryus/screeps-server-mockup for instructions on + * manipulating the terrain and game state. + */ +class IntegrationTestHelper { + private _server: any; + private _player: any; + + get server() { + return this._server; + } + + get player() { + return this._player; + } + + async beforeEach() { + this._server = new ScreepsServer(); + + // reset world but add invaders and source keepers bots + await this._server.world.reset(); + + // create a stub world composed of 9 rooms with sources and controller + await this._server.world.stubWorld(); + + // add a player with the built dist/main.js file + const modules = { + main: readFileSync(DIST_MAIN_JS).toString(), + }; + this._player = await this._server.world.addBot({ username: 'player', room: 'W0N1', x: 15, y: 15, modules }); + + // Start server + await this._server.start(); + } + + async afterEach() { + await this._server.stop(); + } +} + +beforeEach(async () => { + await helper.beforeEach(); +}); + +afterEach(async () => { + await helper.afterEach(); +}); + +before(() => { + stdHooks.hookWrite(); +}); + +export const helper = new IntegrationTestHelper(); diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts new file mode 100644 index 0000000..28a8676 --- /dev/null +++ b/test/integration/integration.test.ts @@ -0,0 +1,18 @@ +import {assert} from "chai"; +import {helper} from "./helper"; + +describe("main", () => { + it("runs a server and matches the game tick", async function () { + for (let i = 1; i < 10; i += 1) { + assert.equal(await helper.server.world.gameTime, i); + await helper.server.tick(); + } + }); + + it("writes and reads to memory", async function () { + await helper.player.console(`Memory.foo = 'bar'`); + await helper.server.tick(); + const memory = JSON.parse(await helper.player.memory); + assert.equal(memory.foo, 'bar'); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..c3478c9 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,13 @@ +--require test/setup-node.js +--require ts-node/register +--ui bdd + +--reporter spec +--bail +--full-trace +--watch-extensions tsx,ts +--colors + +--recursive +--timeout 5000 +--exit diff --git a/test/setup-node.js b/test/setup-node.js new file mode 100644 index 0000000..1975df8 --- /dev/null +++ b/test/setup-node.js @@ -0,0 +1,6 @@ +//inject mocha globally to allow custom interface refer without direct import - bypass bundle issue +global._ = require('lodash'); +global.mocha = require('mocha'); +global.chai = require('chai'); +global.sinon = require('sinon'); +global.chai.use(require('sinon-chai')); diff --git a/test/unit/main.test.ts b/test/unit/main.test.ts new file mode 100644 index 0000000..614c830 --- /dev/null +++ b/test/unit/main.test.ts @@ -0,0 +1,25 @@ +import {assert} from "chai"; +import {loop} from "../../src/main"; +import {Game, Memory} from "./mock" + +describe("main", () => { + before(() => { + // runs before all test in this block + }); + + beforeEach(() => { + // runs before each test in this block + // @ts-ignore : allow adding Game to global + global.Game = _.clone(Game); + // @ts-ignore : allow adding Memory to global + global.Memory = _.clone(Memory); + }); + + it("should export a loop function", () => { + assert.isTrue(typeof loop === "function"); + }); + + it("should return void when called with no context", () => { + assert.isUndefined(loop()); + }); +}); diff --git a/test/unit/mock.ts b/test/unit/mock.ts new file mode 100644 index 0000000..a7a868f --- /dev/null +++ b/test/unit/mock.ts @@ -0,0 +1,10 @@ +export const Game = { + creeps: [], + rooms: [], + spawns: {}, + time: 12345 +}; + +export const Memory = { + creeps: [] +}; diff --git a/tools/node b/tools/node new file mode 100755 index 0000000..a506b9f --- /dev/null +++ b/tools/node @@ -0,0 +1,21 @@ +#!/bin/bash + +set -eu + +source .env + +TTY= +if [ -t 1 ] ; then + TTY=--tty +fi + +docker run \ + --rm \ + --interactive \ + ${TTY} \ + --init \ + --user "$(id -u):$(id -g)" \ + --volume "$PWD:/app" \ + --workdir /app \ + ${NODE_IMAGE} \ + node "$@" diff --git a/tools/npm b/tools/npm new file mode 100755 index 0000000..90262ec --- /dev/null +++ b/tools/npm @@ -0,0 +1,27 @@ +#!/bin/bash + +set -eu + +source .env + +HOST_CACHE_DIR=$PWD/var/docker-cache/.npm +CONTAINER_CACHE_DIR=/tmp/.npm + +mkdir -p ${HOST_CACHE_DIR} + +TTY= +if [ -t 1 ] ; then + TTY=--tty +fi + +docker run \ + --rm \ + --interactive \ + ${TTY} \ + --init \ + --user "$UID:$(id -g)" \ + --volume "$PWD:/app" \ + --env npm_config_cache="${CONTAINER_CACHE_DIR}" \ + --workdir /app \ + ${NODE_IMAGE} \ + npm "$@" diff --git a/tools/tsc b/tools/tsc new file mode 100755 index 0000000..03853c5 --- /dev/null +++ b/tools/tsc @@ -0,0 +1,16 @@ +#!/bin/bash + +set -eu + +source .env + +docker run \ + --rm \ + --interactive \ + --tty \ + --init \ + --user "$(id -u):$(id -g)" \ + --volume "$PWD:/app" \ + --workdir /app \ + ${NODE_IMAGE} \ + ./node_modules/.bin/tsc "$@" diff --git a/tools/yarn b/tools/yarn new file mode 100755 index 0000000..aba93cf --- /dev/null +++ b/tools/yarn @@ -0,0 +1,26 @@ +#!/bin/bash + +set -eu + +source .env + +HOST_CACHE_DIR=$PWD/var/docker-cache/.npm +CONTAINER_CACHE_DIR=/tmp/.npm + +mkdir -p ${HOST_CACHE_DIR} + +TTY= +if [ -t 1 ] ; then + TTY=--tty +fi + +docker run \ + --rm \ + --interactive \ + ${TTY} \ + --init \ + --user "$UID:$(id -g)" \ + --volume "$PWD:/app" \ + --workdir /app \ + ${NODE_IMAGE} \ + yarn "$@" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a8dd055 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "esnext", + "lib": ["esnext"], + "target": "es2017", + "moduleResolution": "Node", + "outDir": "dist", + "baseUrl": "src/", + "sourceMap": true, + "strict": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false + }, + "exclude": [ + "node_modules" + ] +} diff --git a/tsconfig.test-integration.json b/tsconfig.test-integration.json new file mode 100644 index 0000000..875a94b --- /dev/null +++ b/tsconfig.test-integration.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "esnext", + "lib": ["esnext"], + "target": "es5", + "moduleResolution": "Node", + "outDir": "dist", + "baseUrl": "src/", + "sourceMap": true, + "strict": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "noImplicitAny": false, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false + }, + "exclude": [ + "node_modules" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..adbf116 --- /dev/null +++ b/tslint.json @@ -0,0 +1,21 @@ +{ + "rulesDirectory": "tslint-plugin-prettier", + "extends" : [ + "tslint:recommended", + "tslint-config-prettier" + ], + "rules": { + "forin": false, + "interface-name": [true, "never-prefix"], + "member-ordering": [false], + "no-console": [false], + "no-namespace": [true, "allow-declarations"], + "variable-name": [ + true, + "ban-keywords", + "check-format", + "allow-pascal-case", + "allow-leading-underscore" + ] + } +}