391 lines
12 KiB
JavaScript
Executable file
391 lines
12 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
var process = require("node:process");
|
|
var { spawn } = require("node:child_process");
|
|
var { pipeline } = require("node:stream/promises");
|
|
var {
|
|
AIDechunkinator,
|
|
DataGenerator
|
|
} = require("klh-util");
|
|
|
|
var pltags = [...Array(256)].map((e,i)=>String.fromCharCode(i)).filter(e=>(e<='Z'&&e>='A')||(e>='0'&&e<='9'));
|
|
var WIDTH, HEIGHT, APPLES, bots; (async()=>{
|
|
|
|
try {
|
|
var _;
|
|
[ _, _, WIDTH, HEIGHT, APPLES, ...bots ] = process.argv;
|
|
[ WIDTH, HEIGHT, APPLES ] = [ WIDTH, HEIGHT, APPLES ]
|
|
.map(e => { e = parseInt(e); if(e!=e) throw new Error('invalid argument'); return e; });
|
|
if (!bots.length) throw new Error("not enough bot");
|
|
bots = bots.map(cmd => Object.assign(spawn('sh', ['-c', cmd], {
|
|
stdio: ['pipe', 'pipe', 'inherit']
|
|
}), { cmd }));
|
|
if (bots.length > pltags.length) {
|
|
throw new Error(`due to (not enough letters) it is not allowed to have more than ${pltags.length} players. rejecting attempt with ${bots.length} players`);
|
|
}
|
|
console.log(`starting snake (width: ${WIDTH}, height: ${HEIGHT}, apples: ${APPLES})`);
|
|
} catch(e) {
|
|
console.error("Usage: node gaming.js WIDTH HEIGHT APPLES CMD...\n")
|
|
throw e;
|
|
}
|
|
|
|
var grid = Object.assign([...Array(WIDTH * HEIGHT)], {
|
|
width: WIDTH, height: HEIGHT,
|
|
norm(x, y) {
|
|
return [
|
|
(Math.round(x) % this.width + this.width) % this.width,
|
|
(Math.round(y) % this.height + this.height) % this.height
|
|
];
|
|
},
|
|
pos2i(x, y) {
|
|
[x, y] = this.norm(x, y);
|
|
return y * this.width + x;
|
|
},
|
|
i2pos(i) {
|
|
return [i%this.width, Math.floor(i/this.width)];
|
|
},
|
|
set(x, y, v) {
|
|
[x, y] = this.norm(x, y);
|
|
var old = this.get(x, y);
|
|
this[this.pos2i(x, y)] = v;
|
|
return old;
|
|
},
|
|
get(x, y) {
|
|
[x, y] = this.norm(x, y);
|
|
return this[this.pos2i(x, y)];
|
|
},
|
|
});
|
|
|
|
function dir2i(dx, dy) {
|
|
return Math.round(Math.atan2(dy, dx)/(Math.PI*2)*4+4)%4
|
|
}
|
|
|
|
function i2dir(i) {
|
|
return [
|
|
Math.round(Math.cos(i * Math.PI*2/4))+0,
|
|
Math.round(Math.sin(i * Math.PI*2/4))+0
|
|
];
|
|
}
|
|
|
|
function posdiff(src, dst) {
|
|
let [x0,y0, x1,y1] = [...dst, ...src];
|
|
return [x0-x1, y0-y1];
|
|
}
|
|
|
|
function posadd(src, diff) {
|
|
let [x0,y0, x1,y1] = [...src, ...diff];
|
|
return [x0+x1, y0+y1];
|
|
}
|
|
|
|
function mchar(unicode, ascii, o) {
|
|
return {
|
|
utf8: Buffer.from(unicode, 'utf8'),
|
|
utf16le: Buffer.from(unicode, 'utf16le'),
|
|
utf16be: Buffer.from(
|
|
(buf => new Uint16Array(
|
|
buf.buffer, buf.byteOffset,
|
|
buf.length / Uint16Array.BYTES_PER_ELEMENT
|
|
))(
|
|
Buffer.from(unicode, 'utf16le')
|
|
).map(e => ((e&0xFF)<<8)+(e>>8)).buffer
|
|
),
|
|
ascii: Buffer.from(ascii, 'latin1'),
|
|
...(o ?? {})
|
|
};
|
|
}
|
|
|
|
var charmap = {
|
|
apple: mchar('$', '$', { type: 'apple' }),
|
|
space: mchar('.', '.'),
|
|
crossB: mchar('╋', '+', { type: 'body', edges: [1,1,1,1] }),
|
|
crossN: mchar('╬', '#', { type: 'neck', edges: [1,1,1,1] }),
|
|
self0: mchar('0', '0'),
|
|
other: mchar('O', 'O'),
|
|
nline: mchar('\n', '\n'),
|
|
};
|
|
|
|
[
|
|
{ chars: '═║╔╗╝╚', type: 'neck', alt: '=|7931' },
|
|
{ chars: '━┃┏┓┛┗', type: 'body', alt: '-:qecz' },
|
|
].forEach(({chars,type,alt}) => (
|
|
[...chars].map((e, i, a, b, l) => (
|
|
i<2 ? (a=i,b=(i+2)%4) : (a=i-2, b=(i-2+1)%4),
|
|
mchar(e, alt[i], { type, edges: (l=[], l[a]=1, l[b]=1, l) })
|
|
)).forEach((c, a, b) => (
|
|
[a, b] = c.edges.map((e, i) => i).filter(e => e!=null),
|
|
((charmap[type]??=[])[a]??=[])[b]=c,
|
|
((charmap[type]??=[])[b]??=[])[a]=c
|
|
))
|
|
));
|
|
|
|
(chars => ['utf8', 'ascii'].forEach(enc => console.log(`# ${enc.padStart(5)}: `+(chars.map(e=>e[enc]).join('')))))((iter => (iter = function*(obj) {
|
|
for (i in obj) {
|
|
if (obj[i].utf8 && obj[i].ascii+''!='\n') {
|
|
yield obj[i];
|
|
} else {
|
|
for (let o of iter(obj[i])) {
|
|
yield o;
|
|
}
|
|
}
|
|
}
|
|
}, [...new Set(iter(charmap))]))())
|
|
console.log()
|
|
|
|
await Promise.all(bots.map(async (e, i, bots) => {
|
|
pipeline(e.writer = new DataGenerator(), e.stdin);
|
|
e.reader = new AIDechunkinator(e.stdout);
|
|
e.i = i;
|
|
var es = [];
|
|
var sigma = false;
|
|
for await (let c of e.reader.bytes()) {
|
|
c = String.fromCharCode(c);
|
|
if (c == '\0') continue;
|
|
if (c == '\n') {
|
|
sigma = true;
|
|
break;
|
|
};
|
|
if (es.length < 65536)
|
|
es.push(c);
|
|
}
|
|
if (!sigma) {
|
|
console.log(`RESIGNED: bot ${i} (cmd: ${JSON.stringify(e.cmd)})`);
|
|
delete bots[i];
|
|
return;
|
|
}
|
|
var map = {
|
|
utf8: 'utf8',
|
|
utf16: 'utf16ne',
|
|
utf16ne: 'utf16' + (new Uint8Array(new Uint16Array([1]).buffer)[0] ? 'le' : 'be'),
|
|
utf16le: 'utf16le',
|
|
utf16be: 'utf16be',
|
|
ucs2: 'ucs2ne',
|
|
ucs2ne: 'utf16ne',
|
|
ucs2le: 'utf16le',
|
|
ucs2be: 'utf16be',
|
|
ascii: 'ascii',
|
|
latin1: 'ascii',
|
|
};
|
|
e.encoding = map[es.join('').trim()] || 'utf8';
|
|
while (map[e.encoding] != e.encoding)
|
|
e.encoding = map[e.encoding];
|
|
e.writer.push(Buffer.from([...`${WIDTH}\n${HEIGHT}\n`].flatMap(c => [...(mchar(c, c)[e.encoding])])));
|
|
e.debt = 0;
|
|
|
|
posgen: for (let at=0; at < 1000 || (()=>{throw new Error('no valid position found')})(); at++) {
|
|
let [x, y, r] = [WIDTH, HEIGHT, 4].map(x => Math.floor(Math.random()*x));
|
|
let old = Object.setPrototypeOf([], grid);
|
|
e.dir = (r+2)%4;
|
|
e.fast = false;
|
|
for (let [px, py, i] = [x, y, 0]; i < 5; i++, [px, py] = i2dir(r).map((e,i)=>e+[px, py][i])) {
|
|
if (grid.get(px, py)) {
|
|
Object.assign(grid, old);
|
|
continue posgen;
|
|
}
|
|
let [ox, oy] = i2dir(r+2).map((e,i)=>e+[px, py][i]);
|
|
old.set(px, py, grid.set(px, py, px==x&&py==y ? { type: 'head', player: e.i } : ox==x&&oy==y ? charmap.neck[r][(r+2)%4] : charmap.body[r][(r+2)%4] ));
|
|
if (grid.get(px, py).type == 'head') {
|
|
e.head = [px, py];
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
console.log(`welcome bot ${i} (cmd: ${JSON.stringify(e.cmd)}, encoding: ${e.encoding} (${JSON.stringify(es.join(''))}))`);
|
|
}));
|
|
console.log();
|
|
|
|
function spawnApple() {
|
|
for (let at=0; at < 1000 || (()=>{throw new Error('no valid position found')})(); at++) {
|
|
let [x, y] = [WIDTH, HEIGHT].map(x => Math.floor(Math.random()*x));
|
|
if (grid.get(x, y)) continue;
|
|
grid.set(x, y, charmap.apple);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (let i=0; i<APPLES; i++) {
|
|
spawnApple();
|
|
}
|
|
|
|
function showGame() {
|
|
console.log('world state:');
|
|
for (let y=0; y<HEIGHT; y++) {
|
|
let l = [];
|
|
for (let x=0; x<WIDTH; x++) {
|
|
l.push(grid.get(x, y)||charmap.space)
|
|
}
|
|
console.log(l.map(e => e.type == 'head' ? pltags[e.player] : e.utf8).join(''));
|
|
}
|
|
console.log();
|
|
console.log('leaderboard:');
|
|
console.log(bots.filter(e=>e).map(e=>[e,[...readSnake(...e.head)].length]).sort((a,b) => b[1]-a[1]).map((e,i) => `${i}. bot ${e[0].i} (${e[1]})`).join('\n'));
|
|
console.log();
|
|
}
|
|
|
|
function *readSnake(x, y) {
|
|
if (grid.get(x, y).type != 'head')
|
|
throw new Error('not a snake');
|
|
yield [x, y];
|
|
let dx, dy;
|
|
let r = [...Array(4)].map((e,i) => i).find(r => (o => o && o.type == "neck" && o.edges[dir2i(...posdiff([x+dx,y+dy],[x,y]))])(grid.get(...([dx,dy]=i2dir(r),[x+dx,y+dy]))));
|
|
if (r==null) throw new Error('malformed snake');
|
|
[dx, dy] = i2dir(r);
|
|
[x, y] = [x+dx, y+dy];
|
|
while (1) {
|
|
let o = grid.get(x, y);
|
|
yield [x, y];
|
|
if (!o.edges[r]) {
|
|
r = o.edges.map((e,i)=>e?i:undefined).filter(e=>e!=null).find(e => e!=null && e!=(r+2)%4);
|
|
}
|
|
[dx, dy] = i2dir(r);
|
|
[x, y] = [x+dx, y+dy];
|
|
o = grid.get(x, y);
|
|
if (!(o && o.edges && o.edges[(r+2)%4]))
|
|
break;
|
|
}
|
|
}
|
|
|
|
function kill(bot, msg) {
|
|
for (let [x, y] of [...readSnake(...bot.head)]) {
|
|
grid.set(x, y, ((grid.get(x, y).edges??[]).filter(e=>e).length == 4 ? 1 : Math.random(0.5)) > 0.5 ? charmap.apple : [][[]]);
|
|
}
|
|
bot.writer.push();
|
|
delete bots[bot.i];
|
|
if (msg)
|
|
console.log(`${msg}: bot ${bot.i}`);
|
|
}
|
|
|
|
while (bots.filter(e=>e).length) {
|
|
showGame();
|
|
|
|
let moves = await Promise.all(bots.map(async (bot) => {
|
|
console.log(`waiting for bot ${bot.i}`);
|
|
let out = [];
|
|
try {
|
|
for (let y=0; y<HEIGHT; y++) {
|
|
for (let x=0; x<WIDTH; x++) {
|
|
out.push(...(x => x.type == 'head' ? (x.player == bot.i ? charmap.self0 : charmap.other) : x)(grid.get(x, y)||charmap.space)[bot.encoding])
|
|
}
|
|
out.push(...charmap.nline[bot.encoding]);
|
|
}
|
|
out.push(...charmap.nline[bot.encoding]);
|
|
await bot.writer.push(Buffer.from(out));
|
|
} catch {}
|
|
for await (let c of bot.reader.bytes()) {
|
|
c = String.fromCharCode(c);
|
|
if (c in Object.fromEntries([...('ldruLDRU.')].map(e => [e, 1]))) {
|
|
console.log(`READY: bot ${bot.i}`);
|
|
return { key: c, i: bot.i };
|
|
}
|
|
}
|
|
kill(bot, 'RESIGNED');
|
|
}));
|
|
moves.forEach((e,i,l) => e??delete l[i]);
|
|
|
|
moves.forEach((move) => {
|
|
let bot = move.bot = bots[move.i];
|
|
let r = [...'rdlu'].map((e,i)=>[e,i]).find(e => e[0] == move.key.toLowerCase());
|
|
if (r) {
|
|
r=r[1];
|
|
bot.fast = move.key != move.key.toLowerCase();
|
|
if (bot.dir != (r+2)%4)
|
|
bot.dir = r;
|
|
}
|
|
move.body = [...readSnake(...bot.head)];
|
|
if (move.body.length <= 5)
|
|
bot.fast = false;
|
|
bot.debt += 0.5*bot.fast;
|
|
let debt = Math.floor(bot.debt);
|
|
bot.debt -= debt;
|
|
let body = [...readSnake(...bot.head)];
|
|
for (let i=0; body.length>5 && i<debt; i++) {
|
|
let [x, y] = body.pop();
|
|
grid.set(x, y, charmap.apple);
|
|
}
|
|
});
|
|
|
|
for (let mi=0; mi<2; mi++) {
|
|
let cmoves = moves.slice();
|
|
cmoves.forEach((e,i,l) => mi||e.bot.fast||delete l[i]);
|
|
let glover = Object.setPrototypeOf(grid.map(x=>x?({head:1,body:1,neck:1})[x.type]:0), grid);
|
|
let eated = 0;
|
|
|
|
cmoves.forEach((move) => {
|
|
move.body = [...readSnake(...move.bot.head)];
|
|
move.bmap = [];
|
|
move.tail = [];
|
|
move.eated = 0;
|
|
move.nhead = move.bot.head;
|
|
let rhead = grid.get(...move.body[0]);
|
|
move.over = Object.setPrototypeOf(Array(grid.length), grid);
|
|
move.body.forEach(p => move.bmap[grid.pos2i(...p)] = (grid.get(...p).edges??[]).filter(e=>e).length == 4 ? 2 : 1);
|
|
let [x, y] = move.body[0];
|
|
let [dx, dy] = i2dir(move.bot.dir);
|
|
[x, y] = [x+dx, y+dy];
|
|
do {
|
|
let tail = move.body.pop();
|
|
let neck = move.body[1];
|
|
let head = move.body[0];
|
|
let tdir = dir2i(...posdiff(tail, move.body[move.body.length-1]));
|
|
if (!(x => x&&x.type=='apple')(grid.get(x,y))) {
|
|
move.over.set(...tail, ((x,s) => (
|
|
s=x.edges.map((e,i)=>(e&&(i==tdir||i==(tdir+2)%4))?undefined:i).filter(e=>e!=null),
|
|
s.length==2?charmap[x.type=='neck'?'neck':'body'][s[0]][s[1]]:(glover[grid.pos2i(...tail)]--,undefined)
|
|
))(move.over.get(...tail)));
|
|
move.bmap[grid.pos2i(...tail)]--;
|
|
} else {
|
|
move.body.push(tail);
|
|
move.eated++;
|
|
}
|
|
if (move.bmap[grid.pos2i(x,y)]) {
|
|
let wall = move.over.get(x,y);
|
|
if (wall.edges[move.bot.dir]||wall.edges[(move.bot.dir+2)%4]) {
|
|
kill(move.bot, 'DEAD'), delete cmoves[move.i], delete moves[move.i];
|
|
return;
|
|
}
|
|
move.over.set(x, y, charmap.crossN);
|
|
} else {
|
|
move.over.set(x, y, rhead);
|
|
glover[grid.pos2i(x,y)]++
|
|
}
|
|
move.bmap[grid.pos2i(x,y)]??=0;
|
|
move.bmap[grid.pos2i(x,y)]++;
|
|
move.over.set(...neck, ((x,s) => (
|
|
s=x.edges.map((e,i)=>e?i:e).filter(e=>e!=null),
|
|
s.length==4?charmap.crossB:charmap.body[s[0]][s[1]]
|
|
))(move.over.get(...neck)));
|
|
if (move.over.get(...head).type=='head') {
|
|
move.over.set(...head, charmap.neck[dir2i(...posdiff(head, [x, y]))][dir2i(...posdiff(head, neck))]);
|
|
}
|
|
move.nhead = [x, y];
|
|
move.body.unshift(move.nhead);
|
|
[x, y] = [x+dx, y+dy];
|
|
} while (move.over.get(...move.body[0]).type!='head'||(!move.over.get(...move.body[1]).edges.map((e,i)=>i).filter(e=>e!=null).map(di => posadd(move.body[1],i2dir(di))).every(([x,y])=>move.bmap[grid.pos2i(x,y)])));
|
|
Object.setPrototypeOf(move.over, Array.prototype);
|
|
});
|
|
|
|
cmoves.forEach((move) => {
|
|
let gli = grid.pos2i(...move.nhead);
|
|
if ((glover[gli]) > 1) {
|
|
kill(move.bot, 'DEAD'), delete cmoves[move.i], delete moves[move.i];
|
|
return;
|
|
}
|
|
move.over.forEach((e,i,l) => (e==null&&glover[i])&&(delete l[i]));
|
|
Object.assign(grid, move.over);
|
|
move.bot.head = grid.norm(...move.nhead);
|
|
eated += move.eated;
|
|
});
|
|
|
|
let apples;
|
|
if (eated && (apples = grid.filter(e => e&&e.type=='apple').length) < APPLES) {
|
|
for (let i=0; i<(APPLES-apples); i++)
|
|
spawnApple();
|
|
}
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
showGame(); console.log('Game over.')
|
|
|
|
})();
|