forked from jankeromnes/terraforming-mars-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplay-bot.js
187 lines (166 loc) · 6.6 KB
/
play-bot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// Copyright © 2020 Jan Keromnes.
// The following code is covered by the MIT license.
const minimist = require('minimist');
const path = require('path');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const request = require('./lib/request');
const { CardFinder } = require('./terraforming-mars/build/src/CardFinder');
const { PlayerInputTypes } = require('./terraforming-mars/build/src/PlayerInputTypes');
const { SpaceBonus } = require('./terraforming-mars/build/src/SpaceBonus');
const usage = `USAGE
node play-bot [OPTIONS] [PLAYER_LINK]
OPTIONS
-h, --help
Print usage information
--bot=BOT
Play with a specific bot script from the bots/ directory (default is --bot=random)
--games=NUMBER
Play NUMBER of games in a row, then print score statistics
--ignore-errors
If an error occurs during a game, ignore it and just play another game`;
const argv = minimist(process.argv.slice(2));
if (argv.h || argv.help || argv._.length > 1) {
console.log(usage);
process.exit();
}
const cardFinder = new CardFinder();
(async () => {
const scores = [];
const games = argv.games || 1;
while (scores.length < games) {
try {
const game = await playGame(argv._[0], argv.bot);
console.log('Final scores:\n' + game.players.map(p => ` - ${p.name} (${p.color}): ${p.victoryPointsBreakdown.total} points`).join('\n'));
const score = {};
for (const p of game.players) {
score[p.name] = p.victoryPointsBreakdown.total;
}
scores.push(score);
} catch (error) {
if (argv['ignore-errors']) {
continue;
}
throw error;
}
}
if (scores.length > 1) {
console.log(`\nPlayed ${scores.length} game${scores.length === 1 ? '' : 's'}. Score summary:`);
for (const name in scores[0]) {
let min = scores[0][name];
let max = scores[0][name];
let total = scores.map(s => s[name]).reduce((a, b) => {
if (b < min) min = b;
if (b > max) max = b;
return a + b;
}, 0);
let average = Math.round(100 * total / scores.length) / 100;
console.log(` - ${name}: average ${average} points (min ${min}, max ${max})`);
}
}
})();
async function playGame (playerLink, botPath) {
if (!botPath) {
botPath = 'random';
}
if (!playerLink) {
playerLink = (await exec(`node start-game --players=${botPath} --quiet`)).stdout.trim();
console.log('Auto-started new solo game! Bot player link: ' + playerLink);
}
const playerUrl = new URL(playerLink);
const serverUrl = playerUrl.origin;
const playerId = playerUrl.searchParams.get('id');
// Load bot script
const bot = require('./' + path.join('bots', botPath));
// Initial research phase
let game = await waitForTurn(serverUrl, playerId);
logGameState(game);
const availableCorporations = game.waitingFor.options[0].cards;
const availableCards = game.waitingFor.options[1].cards;
annotateCards(game, availableCards);
let move = await bot.playInitialResearchPhase(game, availableCorporations, availableCards);
game = await playMoveAndWaitForTurn(serverUrl, playerId, move);
// Play the game until the end
while (game.phase !== 'end') {
annotateWaitingFor(game, game.waitingFor);
annotateMapSpaces(game);
logGameState(game);
move = await bot.play(game, game.waitingFor);
console.log('Bot plays:', move);
game = await playMoveAndWaitForTurn(serverUrl, playerId, move);
}
console.log('Game ended!');
logGameState(game);
return game;
}
async function playMoveAndWaitForTurn (serverUrl, playerId, move) {
console.log('Bot plays:', move);
let game = await request('POST', `${serverUrl}/player/input?id=${playerId}`, move);
return await waitForTurn(serverUrl, playerId, game);
}
async function waitForTurn (serverUrl, playerId, game) {
while (!game || !('waitingFor' in game) && game.phase !== 'end') {
await new Promise(resolve => setTimeout(resolve, 30));
game = await request('GET', `${serverUrl}/api/player?id=${playerId}`);
}
return game;
}
// Add additional useful information to a game's "waitingFor" object
function annotateWaitingFor (game, waitingFor) {
// Annotate expected player input type (e.g. inputType '2' means playerInputType 'SELECT_AMOUNT')
const playerInputType = PlayerInputTypes[waitingFor.inputType];
if (!playerInputType) {
throw new Error(`Unsupported player input type ${waitingFor.inputType}! Supported types: ${JSON.stringify(PlayerInputTypes, null, 2)}`);
}
waitingFor.playerInputType = playerInputType;
if (waitingFor.cards) {
// Annotate any missing card information (e.g. tags)
annotateCards(game, waitingFor.cards);
}
for (const option of (waitingFor.options || [])) {
// Recursively annotate nested waitingFor options
annotateWaitingFor(game, option);
}
}
// Add additional useful information to cards
function annotateCards (game, cards) {
for (const card of cards) {
// BUG: For some reason, card.calculatedCost is always 0.
// But we can get this info from the dealt project cards or cards in hand.
const cardInHand = game.cardsInHand.find(c => c.name === card.name);
if (card.calculatedCost === 0 && cardInHand && cardInHand.calculatedCost) {
card.calculatedCost = cardInHand.calculatedCost;
}
const dealtProjectCard = game.dealtProjectCards.find(c => c.name === card.name);
if (card.calculatedCost === 0 && dealtProjectCard && dealtProjectCard.cost) {
card.calculatedCost = dealtProjectCard.cost;
}
// Check the reference project card to find & annotate more details.
const projectCard = cardFinder.getProjectCardByName(card.name);
if (!projectCard) {
console.error(new Error(`Could not find card: ${JSON.stringify(card, null, 2)}`));
continue;
}
card.cardType = projectCard.cardType;
// If we still don't know the card's cost, get it from the reference card.
/* FIXME: Why does this reduce the average score of Quantum Bot by 7 points?
if (card.calculatedCost === 0) {
card.calculatedCost = projectCard.cost;
} */
if (!('tags' in card)) {
card.tags = projectCard.tags;
}
if (!('metadata' in card) && ('metadata' in projectCard)) {
card.metadata = projectCard.metadata;
}
}
}
// Add additional useful information to map spaces
function annotateMapSpaces (game) {
game.spaces.forEach(space => {
space.placementBonus = space.bonus.map(b => SpaceBonus[b]);
});
}
function logGameState (game) {
console.log(`Game state (${game.players.length}p): gen=${game.generation}, temp=${game.temperature}, oxy=${game.oxygenLevel}, oceans=${game.oceans}, phase=${game.phase}`);
}