Skip to content

Commit 0f44d81

Browse files
committed
feat(jsbattle-server): run league battles in the background
1 parent 3a95067 commit 0f44d81

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1463
-60
lines changed

packages/jsbattle-engine/src/engine/AiDefinition.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class AiDefinition {
4747
fromJSON(data) {
4848
this._name = data.name;
4949
this._team = data.team;
50-
this._code = data.code;
50+
this._code = data.code ? data.code.replace(/importScripts\w*\([^\)]*\)/g, '') : data.code;
5151
this._initData = data.initData;
5252
this._useSandbox = data.useSandbox;
5353
this._executionLimit = data.executionLimit;

packages/jsbattle-engine/test/engine/AiDefinition.test.js

+15
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,21 @@ describe('AiDefinition', function() {
9191
assert.equal(jsonText1, jsonText2);
9292
});
9393

94+
it('should strip importScripts commands', () => {
95+
let ai = new AiDefinition();
96+
let json = {
97+
name: "text_" + Math.round(Math.random()*1000000),
98+
team: "text_" + Math.round(Math.random()*1000000),
99+
code: "importScripts('whatever')var code = 1;",
100+
initData: null,
101+
useSandbox: false,
102+
executionLimit: 483
103+
};
104+
105+
ai.fromJSON(json);
106+
assert.equal("var code = 1;", ai.code);
107+
});
108+
94109
});
95110

96111
describe('clone', function() {
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
app/services/ubdPlayer/www/**/*

packages/jsbattle-server/.eslintrc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ module.exports = {
149149
"no-loop-func": "error",
150150
"no-magic-numbers": "off",
151151
"no-misleading-character-class": "error",
152-
"no-mixed-operators": "error",
152+
"no-mixed-operators": "off",
153153
"no-mixed-requires": "error",
154154
"no-multi-assign": "error",
155155
"no-multi-spaces": "error",

packages/jsbattle-server/app/lib/ConfigBroker.js

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ class ConfigBroker extends ServiceBroker {
2626
"enabled": true,
2727
"admins": [],
2828
"providers": []
29+
},
30+
"league": {
31+
"scheduleInterval": 5000,
32+
"timeLimit": 30000,
33+
"teamSize": 3
34+
},
35+
"ubdPlayer": {
36+
"queueLimit": 3,
37+
"queueQueryTime": 1000,
38+
"port": 8899,
39+
"speed": 5,
40+
"timeout": 30000
2941
}
3042
};
3143

@@ -50,6 +62,7 @@ class ConfigBroker extends ServiceBroker {
5062

5163
config = _.defaultsDeep(config, defaultConfig)
5264

65+
5366
this.serviceConfig = config;
5467

5568
}

packages/jsbattle-server/app/services/League.service.js

+85-26
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ const _ = require('lodash');
55
const getDbAdapterConfig = require("../lib/getDbAdapterConfig.js");
66
const fs = require('fs');
77
const path = require('path');
8+
const selectOpponents = require('./league/selectOpponents.js');
9+
const updateScores = require('./league/updateScores.js');
810

911
class LeagueService extends Service {
1012

1113
constructor(broker) {
1214
super(broker);
13-
15+
this.config = broker.serviceConfig.league;
16+
this.queueLimit = broker.serviceConfig.ubdPlayer.queueLimit;
1417
let adapterConfig = getDbAdapterConfig(broker.serviceConfig.data, 'league')
1518
this.parseServiceSchema({
1619
...adapterConfig,
@@ -25,7 +28,6 @@ class LeagueService extends Service {
2528
"ownerName",
2629
"scriptId",
2730
"scriptName",
28-
"rank",
2931
"fights_total",
3032
"fights_win",
3133
"fights_lose",
@@ -40,16 +42,19 @@ class LeagueService extends Service {
4042
ownerName: { type: "string", min: 1, max: 255 },
4143
scriptId: "string",
4244
scriptName: { type: "string", min: 1, max: 255 },
43-
rank: "number",
4445
fights_total: "number",
4546
fights_win: "number",
4647
fights_lose: "number",
4748
fights_error: "number",
4849
score: "number",
4950
code: { type: "string", min: 0, max: 65536 },
5051
},
51-
dependencies: ['scriptStore'],
52+
dependencies: [
53+
'scriptStore',
54+
'ubdPlayer'
55+
],
5256
actions: {
57+
scheduleBattle: this.scheduleBattle,
5358
seedLeague: this.seedLeague,
5459
getUserSubmission: this.getUserSubmission,
5560
joinLeague: this.joinLeague,
@@ -61,12 +66,11 @@ class LeagueService extends Service {
6166
create: [
6267
function addDefaults(ctx) {
6368
ctx.params.joinedAt = new Date();
64-
ctx.params.rank = 0;
6569
ctx.params.fights_total = 0;
6670
ctx.params.fights_win = 0;
6771
ctx.params.fights_lose = 0;
6872
ctx.params.fights_error = 0;
69-
ctx.params.score = 0;
73+
ctx.params.score = 1000;
7074
ctx.params = _.omit(ctx.params, ['id']);
7175
return ctx;
7276
}
@@ -76,18 +80,85 @@ class LeagueService extends Service {
7680
events: {
7781
"app.seed": async (ctx) => {
7882
await ctx.call('league.seedLeague', {})
83+
},
84+
"ubdPlayer.battle.league": async (ctx) => {
85+
if(ctx.params.error) {
86+
this.logger.warn('Battle failed between: ' + Object.keys(ctx.params.refData).join(' and '));
87+
return;
88+
}
89+
await updateScores(ctx, this.logger);
7990
}
91+
},
92+
started: () => {
93+
this.loop = setInterval(async () => {
94+
try {
95+
let queueLength = await broker.call('ubdPlayer.getQueueLength', {});
96+
if(queueLength >= this.queueLimit) {
97+
return;
98+
}
99+
await broker.call('league.scheduleBattle', {})
100+
} catch(err) {
101+
this.logger.warn(err)
102+
}
103+
}, this.config.scheduleInterval)
104+
},
105+
stopped: () => {
106+
clearInterval(this.loop)
80107
}
81108
});
82109
}
83110

111+
async scheduleBattle(ctx) {
112+
// pick random opponents
113+
let opponents = await selectOpponents(ctx);
114+
115+
// build UBD
116+
let ubd = {
117+
version: 3,
118+
rngSeed: Math.random(),
119+
teamMode: true,
120+
timeLimit: this.config.timeLimit,
121+
aiList: []
122+
};
123+
124+
let i;
125+
for(i=0; i < this.config.teamSize; i++) {
126+
for(let opponent of opponents) {
127+
ubd.aiList.push({
128+
name: opponent.ownerName === 'jsbattle' ? opponent.scriptName : opponent.ownerName,
129+
team: opponent.ownerName + '/' + opponent.scriptName,
130+
code: opponent.code,
131+
initData: null,
132+
useSandbox: true,
133+
executionLimit: 100
134+
});
135+
}
136+
}
137+
138+
this.logger.debug(`Scheduling battle ${opponents[0].scriptName} vs ${opponents[1].scriptName}`)
139+
140+
try {
141+
let refData = {};
142+
refData[opponents[0].ownerName + '/' + opponents[0].scriptName] = opponents[0].id;
143+
refData[opponents[1].ownerName + '/' + opponents[1].scriptName] = opponents[1].id;
144+
await ctx.call('ubdPlayer.scheduleBattle', {
145+
ubd: ubd,
146+
event: 'league',
147+
refData: refData
148+
});
149+
} catch(err) {
150+
this.logger.debug('Unable to schedule battle due to: ' + err.message)
151+
}
152+
153+
}
154+
84155
async seedLeague(ctx) {
85156
const seedPath = path.resolve(__dirname, 'league', 'seed');
86157
const seedFiles = fs.readdirSync(seedPath)
87-
.map((filename) => ({
88-
ownerId: 0,
158+
.map((filename, index) => ({
159+
ownerId: 'int-user-0000-1',
89160
ownerName: 'jsbattle',
90-
scriptId: 0,
161+
scriptId: 'int-script-0000-' + (index+1),
91162
scriptName: filename.replace(/\.tank$/, ''),
92163
code: fs.readFileSync(path.resolve(seedPath, filename), 'utf8')
93164
}))
@@ -178,10 +249,9 @@ class LeagueService extends Service {
178249
}
179250

180251
let ranktable = await ctx.call('league.find', {
181-
sort: 'rank'
252+
sort: '-score'
182253
});
183-
ranktable = ranktable.map((item) => _.pick(item, [
184-
"rank",
254+
const fields = [
185255
"ownerId",
186256
"ownerName",
187257
"scriptId",
@@ -192,22 +262,11 @@ class LeagueService extends Service {
192262
"fights_lose",
193263
"fights_error",
194264
"score"
195-
]));
265+
]
266+
ranktable = ranktable.map((item) => _.pick(item, fields));
196267

197268
let submission = await this.getUserSubmission(ctx)
198-
submission = _.pick(submission, [
199-
"rank",
200-
"ownerId",
201-
"ownerName",
202-
"scriptId",
203-
"scriptName",
204-
"joinedAt",
205-
"fights_total",
206-
"fights_win",
207-
"fights_lose",
208-
"fights_error",
209-
"score"
210-
]);
269+
submission = _.pick(submission, fields);
211270

212271
return {
213272
submission,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
const Service = require("moleculer").Service;
2+
const { ValidationError, MoleculerError } = require("moleculer").Errors;
3+
const express = require('express')
4+
const path = require('path')
5+
const puppeteer = require('puppeteer');
6+
7+
class UbdPlayer extends Service {
8+
9+
constructor(broker) {
10+
super(broker);
11+
this.app = null;
12+
this.server = null;
13+
this.loop = null;
14+
this.browser = null;
15+
this.isBusy = false;
16+
this.processingStartTime = 0;
17+
this.parseServiceSchema({
18+
name: "ubdPlayer",
19+
dependencies: ['ubdValidator'],
20+
actions: {
21+
scheduleBattle: this.scheduleBattle,
22+
getQueueLength: this.getQueueLength,
23+
},
24+
started: async () => {
25+
// host frontend
26+
this.app = express();
27+
const port = broker.serviceConfig.ubdPlayer.port
28+
this.app.use(express.static(path.join(__dirname, 'ubdPlayer', 'www')))
29+
this.server = this.app.listen(port, 'localhost', () => this.logger.info(`UbdPlayer started at http://localhost:${port}`))
30+
31+
// start browser
32+
this.browser = await puppeteer.launch();
33+
34+
this.loop = setInterval(async () => {
35+
if(this.isBusy) {
36+
let processDuration = new Date().getTime() - this.processingStartTime;
37+
if(processDuration > 1.5 * broker.serviceConfig.ubdPlayer.timeout) {
38+
this.logger.warn('Battle not responding. `Restarting...');
39+
try {
40+
await this.browser.close();
41+
} catch(err) {
42+
this.logger.warn('cannot close browser when restarting');
43+
this.logger.warn(err);
44+
}
45+
try {
46+
this.browser = await puppeteer.launch();
47+
this.isBusy = false;
48+
} catch(err) {
49+
this.logger.warn('cannot restart');
50+
this.logger.warn(err);
51+
}
52+
}
53+
return;
54+
}
55+
if(this.queue.length === 0) {
56+
return;
57+
}
58+
try {
59+
this.processingStartTime = new Date().getTime();
60+
this.isBusy = true;
61+
62+
let task = this.queue.shift();
63+
let ubd = task.ubd;
64+
let page = await this.browser.newPage();
65+
await page.goto('http://localhost:' + port);
66+
await page.waitFor('#ubd');
67+
await page.$eval('#ubd', (el, ubd) => {
68+
el.value = JSON.stringify(ubd)
69+
}, ubd);
70+
await page.$eval('#speed', (el, speed) => {
71+
el.value = speed
72+
}, broker.serviceConfig.ubdPlayer.speed);
73+
74+
this.logger.debug('Battle started');
75+
await page.click('#start');
76+
await page.waitFor('#output', {timeout: broker.serviceConfig.ubdPlayer.timeout});
77+
this.logger.debug('Battle completed');
78+
const element = await page.$("#output");
79+
const text = await page.evaluate((element) => element.innerHTML, element);
80+
await page.close();
81+
const jsonResult = JSON.parse(text)
82+
if(task.refData) {
83+
jsonResult.refData = task.refData;
84+
}
85+
let eventName = "ubdPlayer.battle";
86+
if(task.event) {
87+
eventName = eventName + ("." + task.event)
88+
} else {
89+
eventName = eventName + ".default"
90+
}
91+
broker.broadcast(eventName, jsonResult);
92+
this.isBusy = false;
93+
let dt = new Date().getTime() - this.processingStartTime;
94+
this.logger.debug(`Battle completed after ${dt}ms`)
95+
} catch (err) {
96+
this.logger.debug('Unable to finish the battle');
97+
this.logger.warn(err);
98+
this.isBusy = false;
99+
}
100+
}, broker.serviceConfig.ubdPlayer.queueQueryTime)
101+
102+
},
103+
stopped: () => {
104+
this.server.close();
105+
if(this.loop) {
106+
clearInterval(this.loop);
107+
this.loop = null;
108+
}
109+
if(this.browser) {
110+
this.browser.close();
111+
this.browser = null;
112+
}
113+
}
114+
});
115+
this.queue = []
116+
this.queueLimit = broker.serviceConfig.ubdPlayer.queueLimit
117+
}
118+
119+
getQueueLength() {
120+
return this.queue.length;
121+
}
122+
123+
async scheduleBattle(ctx) {
124+
let result = await ctx.call('ubdValidator.validate', {ubd: ctx.params.ubd})
125+
if(!result.valid) {
126+
throw new ValidationError('Invalid ubd to play!')
127+
}
128+
if(this.queue.length >= this.queueLimit) {
129+
throw new MoleculerError('UbdPlayer queue limit exceeded')
130+
}
131+
this.queue.push({
132+
ubd: ctx.params.ubd,
133+
event: ctx.params.event || "",
134+
refData: ctx.params.refData || undefined,
135+
});
136+
this.logger.debug('Battle scheduled. Queue length: ' + this.queue.length)
137+
}
138+
139+
}
140+
141+
module.exports = UbdPlayer;

0 commit comments

Comments
 (0)