const fetch = require('node-fetch'); const WebSocket = require('ws'); const fs = require('fs').promises; const version = '0.0.1'; if (process.argv.length < 5) { console.error(` usage: npm start SERVER GAME-ID USER For example: npm start https://nuc.ketrenos.com:3000/ketr.ketran robot-wars ai-1 `); process.exit(-1); } const server = process.argv[2]; const gameId = process.argv[3]; let session = undefined; const name = process.argv[4]; const game = {}; process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; const error = (e) => { console.log(`ws - error`, e); } const connect = async () => { let loc = new URL(server), new_uri; let player; try { const data = JSON.parse(await fs.readFile(`${name}.json`, 'utf-8')); player = data.player; } catch (_) { const res = await fetch(`${server}/api/v1/games`, { method: 'GET', cache: 'no-cache', credentials: 'same-origin', /* include cookies */ headers: { 'Content-Type': 'application/json' } }); if (!res) { throw new Error(`Unable to connect to ${server}`); } player = JSON.parse(await res.text()).player; await fs.writeFile(`${name}.json`, JSON.stringify({ name, player })); } console.log(`Connecting to ${server} as ${player}`); if (loc.protocol === "https:") { new_uri = "wss"; } else { new_uri = "ws"; } new_uri = `${new_uri}://${loc.host}/ketr.ketran/api/v1/games/ws/${gameId}`; const ws = new WebSocket(new_uri, [], { 'headers': { 'Cookie': `player=${player}` } }); return new Promise((resolve, reject) => { const headers = (e) => { console.log(`ws - headers`); }; const open = (e) => { console.log(`ws - open`); resolve(ws); }; const connection = (ws) => { console.log("connection request cookie: ", ws.upgradeReq.headers.cookie); }; const close = (e) => { console.log(`ws - close`); }; ws.on('open', open); ws.on('connect', () => { connect(ws); }); ws.on('headers', headers); ws.on('close', close); ws.on('error', error); ws.on('message', async (data) => { await message(ws, data); }); }); }; const createPlayer = (ws) => { const send = (data) => { ws.send(JSON.stringify(data)); }; if (game.name === '') { send({ type: 'player-name', name }); return; } if (game.state !== 'lobby') { return; } if (game.unselected.indexOf(name) === -1) { return; } const slots = []; for (let color in game.players) { if (game.players[color].status === 'Not active') { slots.push(color); } } if (slots.length === 0) { return; } const index = Math.floor(Math.random() * slots.length); console.log(`Requesting to play as ${slots[index]}.`); game.unselected = game.unselected.filter( color => color === slots[index]); send({ type: 'set', field: 'color', value: slots[index] }); send({ type: 'chat', message: `Woohoo! Robot AI ${version} is alive!` }); }; const tryBuild = (ws) => { const send = (data) => { console.log(`ws - send`); ws.send(JSON.stringify(data)); }; let trying = false; if (game.private.settlements && game.private.wood && game.private.brick && game.private.sheep && game.private.wheat) { send({ type: 'buy-settlement' }); trying = true; } if (game.private.wood && game.private.brick && game.private.roads) { send({ type: 'buy-road' }); trying = true; } return trying; }; const sleep = async (delay) => { return new Promise((resolve) => { setTimeout(resolve, delay); }); }; const message = async (ws, data) => { const send = (data) => { console.log(`ws - send: ${data.type}`); ws.send(JSON.stringify(data)); }; data = JSON.parse(data); switch (data.type) { case 'game-update': Object.assign(game, data.update); delete data.update.chat; delete data.update.activities; console.log(`ws - receive - `, data.update ); console.log(`state - ${game.state}`); switch (game.state) { case undefined: case 'lobby': createPlayer(ws); break; case 'game-order': if (!game.color) { console.log(`game-order - player not active`); return; } console.log(`game-order - `, { color: game.color, players: game.players }); if (!game.players[game.color].orderRoll || game.players[game.color].tied) { console.log(`Time to roll as ${game.color}`); send({ type: 'roll' }); } break; case 'initial-placement': { await sleep(1000 + Math.random() * 500); console.log({ color: game.color, state: game.state, turn: game.turn }); if (game.turn.color !== game.color) { break; } let index; const type = game.turn.actions[0]; if (type === 'place-road') { console.log({ roads: game.turn.limits.roads }); index = game.turn.limits.roads[Math.floor( Math.random() * game.turn.limits.roads.length)]; } else if (type === 'place-settlement') { console.log({ corners: game.turn.limits.corners }); index = game.turn.limits.corners[Math.floor( Math.random() * game.turn.limits.corners.length)]; } console.log(`Selecting ${type} at ${index}`); send({ type, index }); } break; case 'normal': if (game.players[game.color].mustDiscard) { await sleep(1000 + Math.random() * 500); let mustDiscard = game.players[game.color].mustDiscard; if (!mustDiscard) { return; } const cards = [], discards = {}; const types = ['wheat', 'sheep', 'stone', 'brick', 'wood']; types.forEach(type => { for (let i = 0; i < game.private[type]; i++) { cards.push(type); } }); while (mustDiscard--) { const type = cards[Math.floor(Math.random() * cards.length)]; if (!(type in discards)) { discards[type] = 1; } else { discards[type]++; } } console.log(`discarding - `, discards); send({ type: 'discard', discards }); return; } if (game.turn.color !== game.color) { console.log(`not ${name}'s turn.`) return; } await sleep(1000 + Math.random() * 500); if (game.turn.color !== game.color) { return; } if (game.turn.actions && game.turn.actions.indexOf('place-road') !== -1) { index = game.turn.limits.roads[Math.floor( Math.random() * game.turn.limits.roads.length)]; send({ type: 'place-road', index }); return; } if (game.turn.actions && game.turn.actions.indexOf('place-settlement') !== -1) { console.log({ corners: game.turn.limits.corners }); index = game.turn.limits.corners[Math.floor( Math.random() * game.turn.limits.corners.length)]; send({ type: 'place-settlement', index }); return; } if (!game.dice) { console.log(`Rolling...`); send({ type: 'roll' }); return; } if (game.turn.actions && game.turn.actions.indexOf('place-robber') !== -1) { console.log({ pips: game.turn.limits.pips }); const index = game.turn.limits.pips[Math.floor(Math.random() * game.turn.limits.pips.length)]; console.log(`placing robber - ${index}`) send({ type: 'place-robber', index }); return; } if (game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1) { if (!game.turn.limits.players) { console.warn(`No players in limits with steal-resource`); return; } const { color } = game.turn.limits.players[Math.floor(Math.random() * game.turn.limits.players.length)]; console.log(`stealing resouce from ${game.players[color].name}`); send({ type: 'steal-resource', color }); return; } if (game.turn.robberInAction) { console.log({ turn: game.turn }); } else { console.log({ turn: game.turn, wheat: game.private.wheat, sheep: game.private.sheep, stone: game.private.stone, brick: game.private.brick, wood: game.private.wood, }); if (!tryBuild(ws)) { send({ type: 'pass' }); } } break; default: console.log({ state: game.state, turn: game.turn }); break; } break; case 'ping': if (!game.state) { console.log(`ping received with no game. Sending update request`); ws.send(JSON.stringify({ type: 'game-update' })); } break; default: console.log(data); break; } } const ai = async (ws) => { } connect().then((ws) => { ai(ws) .catch((error) => { console.error(error); ws.close(); }); }) .catch((error) => { console.error(error); });