I'm currently writing some scripts for Bot Land. Bot Land is a real-time strategy game where instead of controlling your units with a mouse and keyboard, you write code to control your bots via an API, and then your bots go fight others' bots. If you're familiar with units in SC2, you can create bots that are similar to blink stalkers, siege tanks, medics, and ultralisks. (It's quite a fun game for software engineers, but that's outside the scope of this question.)
Bot control has three levels of increasing complexity: a default AI, a Scratch-like programming language, and a reduced set of JavaScript called BotLandScript. Although the built-in editor for BotLandScript is reasonable, you have to upload all your code as one single file with global top-level functions everywhere. Naturally, this starts getting painful after a while if your code starts to get long and different bots share the same functions.
To facilitate writing code for multiple bots, reduce the chance for unintentional errors when coding in bare JS, and increase my chances of beating other players, I set up the above TypeScript project to provide a common library as well as code for each of my bots. The current directory structure looks like approximately like the following:
lib/
bot.land.d.ts
common.ts
BlinkStalker/
BlinkStalker.ts
tsconfig.json
Artillery/
Artillery.ts
tsconfig.json
SmartMelee/
SmartMelee.ts
tsconfig.json
lib
is the common code that is shared among bots, and provides TypeScript definitions for the (non-TS) Bot Land API. Each bot then gets its own folder, with one file containing the bot code and the other a boilerplate tsconfig.json
:
{
"compilerOptions": {
"target": "es3",
"module": "none",
"sourceMap": false,
"outFile": "bot.js"
},
"files": [
"MissileKite.ts"
],
"include": [
"../lib/**/*"
]
}
When each tsconfig.json
is built, it creates a corresponding bot.js
that contains transpiled code from the bot itself as well as all the code in common.js
. This setup is suboptimal for a few reasons, among others: it requires a lot of duplicate boilerplate, makes it hard to add new bots, includes a lot of unnecessary code for each bot, and requires each bot to be built separately.
However, based on my research so far, it doesn't seem like there's an easy way to do what I want. In particular, using the new tsc -b
option and references does not work, because that requires the code to be modularized and Bot Land requires a single file with all functions defined at the top level.
What's the best way to achieve as many of the following as possible?
- No new boilerplate required to add a new bot (e.g. no
tsconfig.json
per bot)
- Use
import
for common functions to avoid outputting unused code, but then...
- Still output all functions as one single file in Bot Land's specific format
- A single build step that produces multiple output files, one for each bot
- Bonus: integrating the build process with VS Code. There is a currently correspondingly boilerplate
tasks.json
for building each sub-project.
I vaguely surmise the answer probably involves something like Grunt in addition to tsc
, but I don't know enough about that to be sure.
Here is my attempt to answer your requirements.
Notable files:
src/tsconfig-botland.json
holds settings for any bot.land script (including your custom declarations which I moved to types/bot-land/index.d.ts
). You may which to change the strict
settings I used.
src/tsconfig.json
holds references to all your bots. This is the file to edit whenever you want to add another bot script
A bot script is at least two files: a minimalist tsconfig.json
and one or more .ts
script files.
For example src/AggroMiner/tsconfig.json
:
{
"extends": "../tsconfig-botland",
"compilerOptions": {
"outFile": "../../build/AggroMiner.js"
},
"files": ["index.ts"],
"include": ["**/*.ts", "../lib/**/*.ts"]
}
In most cases, to start a new bot script you should:
- copy any bot folder (i.e.
src/AggroMiner
) to a new folder under src
- edit the
src/<newBotFolder>/tsconfig.json
to edit the outFile
with the name of your bot
- edit
src/tsconfig.json
and add a reference to src/<newBotFolder>
The following npm
/yarn
script have been set:
build
to build all bots
build-clean
which clear the build
folder before running a build
format
to run Prettier on all .ts
files under src
lint
to run a tslint check on all bot scripts
Now running down your requirements:
- No new boilerplate required to add a new bot (e.g. no tsconfig.json per bot)
To achieve this would require creating some script which would enumerate your bots folder/scripts... and setup the relevant per bot tsconfig.json
and run tsc
. Unless it is strictly necessary, a minimal setup (describe above) might be sufficient.
- Use import for common functions to avoid outputting unused code, but then...
First, be aware that if you start using any module export
/import
statements, you will need additional 3rd party to pack / treeshake in order to achieve a single file output. From what I could gather of Bot.land, your scripts are running on the server. Unless deadcode has an impact on your bot performance, I would not really bother.
- Still output all functions as one single file in Bot Land's specific format
Done.
- A single build step that produces multiple output files, one for each bot
Done.
- Bonus: integrating the build process with VS Code. There is a currently correspondingly boilerplate tasks.json for building each sub-project.
The npm
scripts should appear in vsc's tasks list (at least they do in mine) thus making the tasks.json
unnecessary.
You could actually use project references. Follow these steps to get the same results you were getting for your original files, with all functions at the top level in one file. However, I could not find a solution to import only needed functions in bots. That is, without using imports and exports.
In your tsconfig.json at the root
{
"files": [],
"references": [
{ "path": "./lib" }
{ "path": "./AggroMiner" }
{ "path": "./ArtilleryMicro" }
{ "path": "./MissileKite" }
{ "path": "./SmartMelee" }
{ "path": "./ZapKite" }
]
}
Next, in your lib folder, add a tsconfig.json like so
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"composite": true,
"rootDir": ".",
"outFile": "../build/lib.js",
"target": "es3",
"removeComments": true,
"sourceMap": false,
},
"files": [
"data.ts",
"movement.ts",
"utils.ts"
]
}
We need to make a few adjustments in data.ts, movement.ts and utils.ts so that ts doesn't bother us with compilation errors.
data.ts
/// <reference path="./bot.land.d.ts"/>
(...)
movement.ts
/// <reference path="./data.ts"/>
/// <reference path="./utils.ts"/>
(...)
utils.ts
/// <reference path="./bot.land.d.ts"/>
(...)
Next, we add base.json at the root (the tsconfig.json of the bots will extend it).
base.json
{
"compilerOptions": {
"declaration": true,
"composite": true,
"rootDir": ".",
"target": "es3",
"removeComments": true,
"sourceMap": false,
}
}
and the bots' tsconfig.json (adapt according to bots)
{
"extends": "../base",
"compilerOptions": {
"outFile": "../build/AggroMiner.js",
},
"files": [
"AggroMiner.ts"
],
"references": [
{ "path": "../lib", "prepend": true } //note the prepend: true
]
}
That's it. Now just run
tsc -b