summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortomsmeding <hallo@tomsmeding.nl>2015-10-18 21:20:18 +0200
committertomsmeding <hallo@tomsmeding.nl>2015-10-18 21:20:18 +0200
commitb5077ff9a36c89c84c04f33dce3a2a0ff45e9184 (patch)
tree9cdbecbd6a188ca8de874d01a6cdc04adb1582f2
Initial
-rw-r--r--.gitignore1
-rw-r--r--common.js43
-rw-r--r--index.html213
-rwxr-xr-xinteractor.js96
-rw-r--r--package.json15
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"
+ }
+}