// Single-file Kobaltmeer starter for Node.js: protocol loop and editable collection policy (same strategy as the Go/C++ starters). import readline from 'node:readline'; const key = ([x, y]) => `${x},${y}`; const distance = ([ax, ay], [bx, by], size) => { const dx = Math.abs(ax - bx), dy = Math.abs(ay - by); return Math.min(dx, size - dx) + Math.min(dy, size - dy); }; const destination = ([x, y], action, size) => { if (action === 'N') y--; else if (action === 'S') y++; else if (action === 'E') x++; else if (action === 'W') x--; return [(x + size) % size, (y + size) % size]; }; // Ships that are on their way home keep going until they reach a harbor. const returning = new Set(); function decide({ player, rules, state }) { const own = state.players[player]; const harbors = state.sites.filter(site => site.owner === player).map(site => [site.x, site.y]); const fleet = state.ships.filter(ship => ship.owner === player) .sort((a, b) => b.cargo - a.cargo || a.id - b.id); // loaded ships choose first const occupied = new Set(fleet.map(ship => key([ship.x, ship.y]))); const reserved = new Set(); const ships = {}, debug = []; for (const ship of fleet) { const pos = [ship.x, ship.y]; occupied.delete(key(pos)); const home = harbors.reduce((best, h) => distance(pos, h, rules.size) < distance(pos, best, rules.size) ? h : best); if (key(pos) === key(home)) returning.delete(ship.id); if (ship.cargo >= rules.capacity * 3 / 4 || (ship.cargo > 0 && rules.turns - state.turn <= distance(pos, home, rules.size) + 6)) returning.add(ship.id); const value = state.resource[pos[1]][pos[0]]; const fuel = rules.floor_move_cost ? Math.floor(value / rules.move_divisor) : Math.ceil(value / rules.move_divisor); let chosen = 'O', best = -Infinity; for (const action of ['O', 'N', 'E', 'S', 'W']) { const target = destination(pos, action, rules.size); if ((action !== 'O' && ship.cargo < fuel) || occupied.has(key(target)) || reserved.has(key(target))) continue; let score; if (returning.has(ship.id)) score = -distance(target, home, rules.size) * 100; else { score = state.resource[target[1]][target[0]]; if (action !== 'O') score /= 1.4; if (action === 'O' && value < 45) score -= 100; } if (score > best) { best = score; chosen = action; } } const target = destination(pos, chosen, rules.size); reserved.add(key(target)); ships[ship.id] = chosen; debug.push(returning.has(ship.id) ? { ship: ship.id, target: home, reason: 'JS: Fracht abliefern' } : { ship: ship.id, target, reason: 'JS: Nachbarfeld oder Abbau' }); } const spawn = fleet.length < 8 && state.turn * 100 < rules.turns * 55 && own.bank >= rules.ship_cost && !reserved.has(key(own.yard)); return { type: 'orders', ships, spawn, debug }; } let ready = false; for await (const line of readline.createInterface({ input: process.stdin })) { const input = JSON.parse(line); if (!ready) { if (input.type !== 'init' || input.protocol !== 'kobaltmeer-bot-v1') process.exit(2); // input.seed is available for reproducible random decisions; this example is deterministic. ready = true; console.log(JSON.stringify({ type: 'ready', name: 'JS Sammler' })); continue; } if (input.type === 'close') break; console.log(JSON.stringify(decide(input))); }