snake-gaming/gaming.js
2025-09-21 18:16:19 +05:00

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.')
})();