From 3cd6a07fbf33b01aaeca7c301e15cd0ae516fdbe Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sun, 31 May 2020 15:06:55 +0300 Subject: [PATCH] Init files --- .editorconfig | 18 ++++++ .env | 1 + .gitignore | 92 +++++++++++++++++++++++++++ .prettierrc | 7 +++ Dockerfile | 6 ++ Makefile | 16 +++++ README.md | 55 ++++++++++++++++ package.json | 66 ++++++++++++++++++++ rollup.config.js | 32 ++++++++++ rollup.test-integration-config.js | 34 ++++++++++ rollup.test-unit-config.js | 32 ++++++++++ screeps.sample.json | 27 ++++++++ src/harvester.ts | 93 ++++++++++++++++++++++++++++ src/main.ts | 77 +++++++++++++++++++++++ src/types.d.ts | 20 ++++++ src/utils/ErrorMapper.ts | 90 +++++++++++++++++++++++++++ test/integration/helper.ts | 59 ++++++++++++++++++ test/integration/integration.test.ts | 18 ++++++ test/mocha.opts | 13 ++++ test/setup-node.js | 6 ++ test/unit/main.test.ts | 25 ++++++++ test/unit/mock.ts | 10 +++ tools/node | 21 +++++++ tools/npm | 27 ++++++++ tools/tsc | 16 +++++ tools/yarn | 26 ++++++++ tsconfig.json | 19 ++++++ tsconfig.test-integration.json | 20 ++++++ tslint.json | 21 +++++++ 29 files changed, 947 insertions(+) create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 rollup.test-integration-config.js create mode 100644 rollup.test-unit-config.js create mode 100644 screeps.sample.json create mode 100644 src/harvester.ts create mode 100644 src/main.ts create mode 100644 src/types.d.ts create mode 100644 src/utils/ErrorMapper.ts create mode 100644 test/integration/helper.ts create mode 100644 test/integration/integration.test.ts create mode 100644 test/mocha.opts create mode 100644 test/setup-node.js create mode 100644 test/unit/main.test.ts create mode 100644 test/unit/mock.ts create mode 100755 tools/node create mode 100755 tools/npm create mode 100755 tools/tsc create mode 100755 tools/yarn create mode 100644 tsconfig.json create mode 100644 tsconfig.test-integration.json create mode 100644 tslint.json 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" + ] + } +}