diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | common.js | 43 | ||||
-rw-r--r-- | index.html | 213 | ||||
-rwxr-xr-x | interactor.js | 96 | ||||
-rw-r--r-- | package.json | 15 |
5 files changed, 368 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/common.js b/common.js new file mode 100644 index 0000000..dbc3484 --- /dev/null +++ b/common.js @@ -0,0 +1,43 @@ +var W=7,H=8; + +function emptyboard(){ + return new Array(H).fill(0).map(function(){ + return new Array(W).fill(0).map(function(){ + return {n:0,c:0}; + }); + }); +} + +function bdcopy(bd){ + return bd.map(function(r){ + return r.map(function(c){ + return {n:c.n,c:c.c}; + }); + }); +} + +function stabilise(bd){ + var newbd; + var changes; + var x,y,nnei,quo; + do { + changes=false; + newbd=bdcopy(bd); + for(y=0;y<H;y++){ + for(x=0;x<W;x++){ + nnei=(y>0)+(x>0)+(y<H-1)+(x<W-1); + if(bd[y][x].n>=nnei){ + quo=~~(bd[y][x].n/nnei); + newbd[y][x].n-=quo*nnei; + if(y>0) {newbd[y-1][x].n+=quo;newbd[y-1][x].c=bd[y][x].c;} + if(x>0) {newbd[y][x-1].n+=quo;newbd[y][x-1].c=bd[y][x].c;} + if(y<H-1){newbd[y+1][x].n+=quo;newbd[y+1][x].c=bd[y][x].c;} + if(x<W-1){newbd[y][x+1].n+=quo;newbd[y][x+1].c=bd[y][x].c;} + changes=true; + } + } + } + bd=newbd; + } while(changes); + return bd; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..c861c82 --- /dev/null +++ b/index.html @@ -0,0 +1,213 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>Interactor for Chain Reaction</title> +<script src="/socket.io/socket.io.js"></script> +<script src="/common.js"></script> +<script> +var CVSH=500; + +var COLOURS=["#00F","#F00","#0CC"]; + +var socket=io(); +var CELLSZ=~~(CVSH/(H+1)); +var CVSW=CELLSZ*(W+1); +var CELL0X=~~(CVSW/2-W/2*CELLSZ)+.5,CELL0Y=~~(CVSH/2-H/2*CELLSZ)+.5; +var bd; + +var cvs,ctx; + +var onturn; + +var usercanmove; + +function init(){ + cvs=document.getElementById("cvs"); + cvs.width=CVSW; + cvs.height=CVSH; + ctx=cvs.getContext("2d"); + cvs.addEventListener("click",function(ev){ + if(!usercanmove)return; + var bbox=ev.target.getBoundingClientRect(); + var cx=ev.clientX-bbox.left,cy=ev.clientY-bbox.top; + var x=~~((cx-CELL0X)/CELLSZ),y=~~((cy-CELL0Y)/CELLSZ); + if(x<0||y<0||x>=W||y>=H)return; + if(bd[y][x].n&&bd[y][x].c!=onturn)return; + applymove(W*y+x,onturn); + socket.emit("usermove",W*y+x); + usercanmove=false; + setstatustext("<i>Waiting for other move...</i>"); + }); + onturn=0; + usercanmove=false; + bd=emptyboard(); + drawboard(bd,onturn); + setstatustext("<i>Initialised.</i>"); +} + +function drawboard(bd,clr){ + var x,y,i,angle,radius; + ctx.clearRect(0,0,CVSW,CVSH); + ctx.strokeStyle=COLOURS[+clr]; + ctx.beginPath(); + for(y=0;y<H;y++){ + for(x=0;x<W;x++){ + ctx.moveTo(CELL0X+x*CELLSZ,CELL0Y+y*CELLSZ); + ctx.lineTo(CELL0X+(x+1)*CELLSZ,CELL0Y+y*CELLSZ); + ctx.lineTo(CELL0X+(x+1)*CELLSZ,CELL0Y+(y+1)*CELLSZ); + ctx.lineTo(CELL0X+x*CELLSZ,CELL0Y+(y+1)*CELLSZ); + ctx.lineTo(CELL0X+x*CELLSZ,CELL0Y+y*CELLSZ); + } + } + ctx.stroke(); + for(y=0;y<H;y++){ + for(x=0;x<W;x++){ + ctx.fillStyle=COLOURS[bd[y][x].c]; + angle=1/bd[y][x].n*2*Math.PI; + radius=bd[y][x].n==1?0:.12; + for(i=0;i<bd[y][x].n;i++){ + ctx.beginPath(); + ctx.arc( + CELL0X+(x+.5+radius*Math.cos(angle*i))*CELLSZ, + CELL0Y+(y+.5+radius*Math.sin(angle*i))*CELLSZ, + CELLSZ*.2,0,2*Math.PI,1); + ctx.fill(); + } + } + } +} + +function stabiliseanims(bd){ + var anims=[],stage; + var newbd; + var x,y,nnei,quo; + do { + stage=[]; + newbd=bdcopy(bd); + for(y=0;y<H;y++){ + for(x=0;x<W;x++){ + nnei=(y>0)+(x>0)+(y<H-1)+(x<W-1); + if(bd[y][x].n>=nnei){ + quo=~~(bd[y][x].n/nnei); + newbd[y][x].n-=quo*nnei; + if(y>0) { + newbd[y-1][x].n+=quo; newbd[y-1][x].c=bd[y][x].c; + stage.push([W*y+x,W*(y-1)+x]); + } + if(x>0) { + newbd[y][x-1].n+=quo; newbd[y][x-1].c=bd[y][x].c; + stage.push([W*y+x,W*y+x-1]); + } + if(y<H-1){ + newbd[y+1][x].n+=quo; newbd[y+1][x].c=bd[y][x].c; + stage.push([W*y+x,W*(y+1)+x]); + } + if(x<W-1){ + newbd[y][x+1].n+=quo; newbd[y][x+1].c=bd[y][x].c; + stage.push([W*y+x,W*y+x+1]); + } + } + } + } + bd=newbd; + anims.push([stage,bdcopy(bd)]); + } while(stage.length); + return anims; +} + +var applymove_queue=[]; +var applymove_busy=false; + +function applymove(idx,c){ + assert(idx>=0&&idx<W*H); + applymove_busy=true; + bd[~~(idx/W)][idx%W].n++; + bd[~~(idx/W)][idx%W].c=c; + drawboard(bd,onturn); + var anims=stabiliseanims(bd); + anims.forEach(function(pair,i){ + var stage=pair[0],newbd=pair[1]; + var finalstage=i==anims.length-1; + setTimeout(function(){ + var time=0; + var interval=setInterval(function(){ + var i,fx,fy,tx,ty; + for(i=0;i<stage.length;i++){ + fx=stage[i][0]%W; fy=~~(stage[i][0]/W); + bd[fy][fx].n--; + } + drawboard(bd,onturn); + for(i=0;i<stage.length;i++){ + fx=stage[i][0]%W; fy=~~(stage[i][0]/W); + tx=stage[i][1]%W; ty=~~(stage[i][1]/W); + ctx.fillStyle=COLOURS[bd[fy][fx].c]; + ctx.beginPath(); + ctx.arc( + CELL0X+(fx*(1-time/10)+tx*time/10+.5)*CELLSZ, + CELL0Y+(fy*(1-time/10)+ty*time/10+.5)*CELLSZ, + CELLSZ*.2,0,2*Math.PI,1); + ctx.fill(); + } + if(time==10){ + clearInterval(interval); + bd=newbd; + drawboard(bd,onturn); + if(finalstage){ + applymove_busy=false; + if(applymove_queue.length){ + setTimeout(function(){ + applymove(applymove_queue[0][0],applymove_queue[0][1]); + applymove_queue.shift(); + },50); + } + } + } + time++; + },30); + },500*i+1); + }); +} + +function queueapplymove(idx,c){ + if(applymove_busy)applymove_queue.push([idx,c]); + else applymove(idx,c); +} + +function getusermove(){ + usercanmove=true; + setstatustext("<b>Your turn!</b>"); +} + +function setstatustext(text){ + var elem=document.getElementById("statustext"); + elem.innerHTML=text; +} + +function assert(cond){if(!cond)throw new Error("Assertion failed");} + + +socket.on("emptyboard",function(){ + bd=emptyboard(); +}); +socket.on("applymove",function(obj){ + queueapplymove(obj.index,obj.player); +}); +socket.on("alert",function(text){ + alert(text); +}); +socket.on("setonturn",function(player){ + onturn=player; + drawboard(bd,onturn); +}); +socket.on("getusermove",function(){ + getusermove(); +}); +</script> +</head> +<body onload="init()"> +<h3>Interactor for Chain Reaction</h3> +<canvas id="cvs"></canvas><br> +<span id="statustext"></span> +</body> +</html>
\ No newline at end of file diff --git a/interactor.js b/interactor.js new file mode 100755 index 0000000..2d38cbb --- /dev/null +++ b/interactor.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +var aicmd; + +if(process.argv.length!=3){ + console.log("Give AI command to run as command line argument."); + process.exit(1); +} +aicmd=process.argv[2]; +console.log("Using AI command '"+aicmd+"'"); + + +var app=require("express")(), + http=require("http").Server(app), + io=require("socket.io")(http), + fs=require("fs"), + spawn=require("child_process").spawn; + +eval(String(fs.readFileSync("common.js"))); + + +var HTTPPORT=8080; + +var uniqid=(function(){ + var id=0; + return function(){return id++;}; +})(); + +app.get("/",function(req,res){ + res.sendFile(__dirname+"/index.html"); +}); + +["index.html","common.js"].forEach(function(fname){ + app.get("/"+fname,function(req,res){ + res.sendFile(__dirname+"/"+fname); + }); +}); + +io.on("connection",function(conn){ + var id=uniqid(); + console.log("New IO connection id "+id); + + var bd=emptyboard(); + var aiplayer=0; + + conn.on("disconnect",function(){ + console.log("Disconnect id "+id); + }); + conn.on("usermove",function(idx){ + idx=+idx; + if(idx<0||idx>=W*H){ + conn.emit("alert","You sent an invalid move, index "+idx); + conn.emit("getusermove"); + return; + } + var x=idx%W,y=~~(idx/W); + bd[y][x].c=1-aiplayer; + bd[y][x].n++; + bd=stabilise(bd); + proc.stdin.write(x+" "+y+"\n"); + }); + + + var proc=spawn("sh",["-c",aicmd],{ + stdio:["pipe","pipe","inherit"] + }); + var buffer=""; + proc.stdout.on("data",function(data){ + var idx,line,mv; + buffer+=data; + while((idx=buffer.indexOf("\n"))!=-1){ + line=buffer.slice(0,idx); + buffer=buffer.slice(idx+1); + mv=line.split(" ").map(function(s){return parseInt(s,10);}); + if(mv.length!=2||isNaN(mv[0])||isNaN(mv[1])){ + console.log("Invalid move written by AI: '"+line+"'"); + conn.emit("alert","Invalid move written by AI: '"+line+"'"); + mv[0]=0; + mv[1]=0; + } + bd[mv[1]][mv[0]].c=aiplayer; + bd[mv[1]][mv[0]].n++; + bd=stabilise(bd); + conn.emit("applymove",{index:W*mv[1]+mv[0],player:aiplayer}); + conn.emit("setonturn",1-aiplayer); + conn.emit("getusermove"); + } + }); + proc.stdin.write("A\n-1 -1\n"); + conn.emit("emptyboard"); + conn.emit("setonturn",0); +}); + +http.listen(HTTPPORT,function(){ + console.log("Listening on http://localhost:"+HTTPPORT); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..38f7446 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "interactor", + "version": "1.0.0", + "description": "", + "main": "interactor.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Tom Smeding <hallo@tomsmeding.nl> (http://tomsmeding.com)", + "license": "MIT", + "dependencies": { + "express": "^4.13.3", + "socket.io": "^1.3.7" + } +} |