summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortomsmeding <hallo@tomsmeding.nl>2015-12-05 20:40:32 +0100
committertomsmeding <hallo@tomsmeding.nl>2015-12-05 20:40:32 +0100
commit1954ed67ef61c509560418a9d2da58083b822996 (patch)
tree870237504df86fcdd6cfaab2948e73e7211cf95d
Initial enzo
-rw-r--r--.gitignore1
-rw-r--r--common.js82
-rw-r--r--index.html545
-rwxr-xr-xindex.js248
-rw-r--r--interactorindex.html228
-rw-r--r--package.json24
6 files changed, 1128 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..e77db07
--- /dev/null
+++ b/common.js
@@ -0,0 +1,82 @@
+var W=8,H=9;
+
+if(typeof module=="undefined")module=false; //hack to support client-side importing
+
+if(module)module.exports["emptyboard"]=emptyboard;
+function emptyboard(){
+ return new Array(H).fill(0).map(function(){
+ return new Array(W).fill(0).map(function(){
+ return {n:0,c:0};
+ });
+ });
+}
+
+if(module)module.exports["bdcopy"]=bdcopy;
+function bdcopy(bd){
+ return bd.map(function(r){
+ return r.map(function(c){
+ return {n:c.n,c:c.c};
+ });
+ });
+}
+
+if(module)module.exports["checkwin"]=checkwin;
+function checkwin(bd){
+ var wincolour=-1,i;
+ for(i=0;i<W*H;i++){
+ if(bd[~~(i/W)][i%W].n){
+ if(wincolour==-1)wincolour=bd[~~(i/W)][i%W].c
+ else if(bd[~~(i/W)][i%W].c!=wincolour)return -1;
+ }
+ }
+ return wincolour;
+}
+
+if(module)module.exports["countballs"]=countballs;
+function countballs(bd,p){
+ var count=0;
+ var x,y;
+ if(p==undefined){
+ for(y=0;y<H;y++)for(x=0;x<W;x++)count+=bd[y][x].n;
+ } else {
+ for(y=0;y<H;y++)for(x=0;x<W;x++)count+=bd[y][x].n*(bd[y][x].c==p);
+ }
+ return count;
+}
+
+if(module)module.exports["stabilise"]=stabilise;
+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;
+ if(checkwin(bd)!=-1)break;
+ } while(changes);
+ return bd;
+}
+
+
+
+if(module)module.exports["validatenick"]=validatenick;
+function validatenick(nick){
+ return /^[a-zA-Z0-9_-]{3,}$/.test(nick);
+}
+
+Array.prototype.fill=Array.prototype.fill||function(t){if(null==this)throw new TypeError("this is null or not defined");for(var r=Object(this),n=r.length>>>0,i=arguments[1],a=i>>0,e=0>a?Math.max(n+a,0):Math.min(a,n),o=arguments[2],h=void 0===o?n:o>>0,l=0>h?Math.max(n+h,0):Math.min(h,n);l>e;)r[e]=t,e++;return r};
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..dcf0b9b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,545 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Multichain</title>
+<script src="/socket.io/socket.io.js"></script>
+<script src="/common.js"></script>
+<script>
+"use strict";
+
+var COLOURS=["#00F","#F00","#0CC"];
+
+var socket=io(),me=[-1,""],userlist=[],rooms={},visibleroomuidiv=null;
+socket.on("disconnect",function(){
+ location.href=location.href;
+});
+socket.on("message",function(msg){
+ alert("Message:\n\n"+msg);
+});
+socket.on("me",updateme);
+socket.on("userlist",updateuserlist);
+socket.on("room",joinroom);
+socket.on("roomdestroy",roomdestroy);
+socket.on("roominvite",roominvite);
+socket.on("roomjoin",roomadduser);
+socket.on("roomchat",roomchat);
+socket.on("roomgameturn",roomgameturn);
+
+if(location.hostname=="localhost"&&Math.random()<.25)localStorage.setItem("nickname","aaa");
+
+if(localStorage.getItem("nickname"))socket.emit("setnick",localStorage.getItem("nickname"));
+else socket.emit("me",updateme);
+socket.emit("userlist",updateuserlist);
+
+function updateme(_me){
+ me=_me;
+ document.getElementById("nickname").innerHTML=me[1];
+ localStorage.setItem("nickname",me[1]);
+}
+
+function updateuserlist(_ul){
+ userlist=_ul;
+ var elem=document.getElementById("userlist"),l=elem.children;
+ var i,div;
+ for(i=l.length-1;i>=0;i--)elem.removeChild(l[i]);
+ var input=document.getElementById("roomuserinput"),selectedlist=input.value.trim().split(/\s+/g);
+ for(i=0;i<userlist.length;i++){
+ if(userlist[i][1]==me[1]){
+ userlist.splice(i,1);
+ i--;
+ continue;
+ }
+ div=document.createElement("div");
+ div.setAttribute("userid",userlist[i][0]);
+ div.setAttribute("username",userlist[i][1]);
+ div.innerHTML=userlist[i][1];
+ div.addEventListener("click",function(ev){
+ var usernameattr=ev.target.getAttribute("username");
+ if(ev.target.classList.contains("selected")){
+ ev.target.classList.remove("selected");
+ input.value=input.value.trim().split(/\s+/g).filter(function(n){return n!=usernameattr;}).join(" ");
+ } else {
+ ev.target.classList.add("selected");
+ input.value=(input.value.trim()+" "+usernameattr).trim();
+ }
+ });
+ if(selectedlist.indexOf(userlist[i][1])!=-1)div.classList.add("selected");
+ elem.appendChild(div);
+ }
+ input.value=selectedlist.filter(function(n){
+ var i;
+ for(i=0;i<userlist.length;i++)if(userlist[i][1]==n)return true;
+ return false;
+ }).join(" ");
+}
+
+function joinroom(roomid,users){
+ rooms[roomid]=new Room(roomid);
+ users.forEach(function(user){
+ rooms[roomid].join(user);
+ });
+}
+
+function roomadduser(roomid,user){
+ rooms[roomid].join(user);
+}
+
+function Room(_roomid){
+ if(!(this instanceof Room))return new Room(_roomid)
+ this.id=_roomid;
+ this.userlist=[];
+ this.bd=emptyboard();
+ this.onturn=0;
+ this.cellsz=40;
+
+ this.canplace=false;
+
+ this.listdiv=document.createElement("div");
+ this.listdiv.classList.add("room");
+ this.listdiv.innerHTML="<div style='float:right;cursor:default' onclick='dodestroyroom(\""+this.id+"\")'>X</div><b>Room:</b>";
+ this.listdiv.addEventListener("click",function(ev){
+ if(ev.target!=this.listdiv)return;
+ if(visibleroomuidiv)visibleroomuidiv.classList.add("invisible");
+ else document.getElementById("roomuisplash").classList.remove("invisible");
+ visibleroomuidiv=this.uidiv;
+ this.uidiv.classList.remove("invisible");
+ }.bind(this));
+ document.getElementById("roomlistcontainer").appendChild(this.listdiv);
+
+ var e,table,tbody,input;
+ this.uidiv=document.createElement("div");
+ this.uidiv.classList.add("roomui");
+ this.uidiv.classList.add("invisible");
+
+ e=document.createElement("div");
+ e.setAttribute("style","float:right;margin:5px;cursor:pointer");
+ e.innerHTML="x";
+ e.addEventListener("click",function(){
+ this.uidiv.classList.add("invisible");
+ visibleroomuidiv=null;
+ document.getElementById("roomuisplash").classList.add("invisible");
+ }.bind(this));
+ this.uidiv.appendChild(e);
+
+ e=document.createElement("div");
+ e.classList.add("chatdiv");
+ table=document.createElement("table");
+ table.classList.add("chatlogtable");
+ tbody=document.createElement("tbody");
+ tbody.classList.add("chatlogtbody");
+ table.appendChild(tbody);
+ e.appendChild(table);
+ input=document.createElement("input");
+ input.type="text";
+ input.classList.add("chatinput");
+ input.setAttribute("placeholder","Type something...");
+ input.addEventListener("keypress",function(ev){
+ if(ev.keyCode!=13&&ev.which!=13)return;
+ var msg=ev.target.value;
+ ev.target.value="";
+ if(msg.length==0)return;
+ socket.emit("roomchat",this.id,msg);
+ }.bind(this));
+ e.appendChild(input);
+ this.uidiv.appendChild(e);
+
+ e=document.createElement("div");
+ e.classList.add("gamediv");
+ this.cvs=document.createElement("canvas");
+ this.ctx=this.cvs.getContext("2d");
+ this.cvs.classList.add("gamecvs");
+ this.cvs.width=this.cellsz*W+1;
+ this.cvs.height=this.cellsz*H+1;
+ this.cvs.addEventListener("click",function(ev){
+ if(ev.target!=this.cvs)return;
+ var x=ev.clientX/this.cellsz,y=ev.clientY/this.cellsz,idx=W*y+x;
+ if(x<0||y<0||x>=W||y>=H)return;
+ if(this.bd[idx]place(idx,this.onturn);
+ }.bind(this));
+ drawboard(this.ctx,this.cellsz,this.bd,0);
+ e.appendChild(this.cvs);
+ this.uidiv.appendChild(e);
+
+ this.statusdiv=document.createElement("div");
+ this.statusdiv.classList.add("gamestatusdiv");
+ this.uidiv.appendChild(this.statusdiv);
+
+ document.body.appendChild(this.uidiv);
+}
+Room.prototype.join=function(user){
+ this.listdiv.innerHTML+=" "+user[1];
+ this.userlist.push(user.slice());
+};
+Room.prototype.destroy=function(){
+ if(visibleroomuidiv==this.uidiv){
+ visibleroomuidiv=null;
+ document.getElementById("roomuisplash").classList.add("invisible");
+ }
+ this.uidiv.parentElement.removeChild(this.uidiv);
+ this.listdiv.parentElement.removeChild(this.listdiv);
+};
+Room.prototype.chat=function(by,msg){
+ var tr,td,div;
+ tr=document.createElement("tr");
+ td=document.createElement("td");
+ div=document.createElement("div");
+ div.classList.add("chatitem");
+ div.innerHTML="<i>"+by[1]+"</i>: "+msg;
+ td.appendChild(div);
+ tr.appendChild(td);
+ this.uidiv.getElementsByClassName("chatlogtbody")[0].appendChild(tr);
+ tr.scrollIntoView();
+};
+Room.prototype.place=function(pos,pidx){
+ queueapplymove(this.id,this.ctx,this.cellsz,pos,pidx);
+ queueapplymove(this.id,function(){
+ //WINCHECK?
+ this.onturn=(this.onturn+1)%this.userlist.length;
+ drawboard(this.ctx,this.cellsz,this.bd,this.onturn);
+ }.bind(this));
+};
+Room.prototype.myturn=function(ot){
+ this.canplace=true;
+ this.onturn=ot;
+ this.statusdiv.innerHTML="Your turn!";
+};
+
+function roominvite(roomid,by){
+ if(!confirm("User '"+by[1]+"' sent you an invite to join their room. Accept?"))return;
+ socket.emit("inviteaccept",roomid);
+}
+
+function roomdestroy(roomid){
+ rooms[roomid].destroy();
+ delete rooms[roomid];
+}
+
+function roomchat(roomid,by,msg){
+ rooms[roomid].chat(by,msg);
+}
+
+function roomgameturn(roomid,ot){
+ rooms[roomid].myturn(ot);
+}
+
+
+function changenick(){
+ var newnick=prompt("New nickname?");
+ if(!newnick)return;
+ if(!validatenick(newnick)){
+ alert("That's an invalid nick! (regex [a-zA-Z0-9_-]{3,})");
+ return;
+ }
+ socket.emit("setnick",newnick);
+}
+
+function createroom(){
+ var users=document.getElementById("roomuserinput").value.trim().split(/\s+/g).map(function(n){
+ var i;
+ for(i=0;i<userlist.length;i++)if(userlist[i][1]==n)return userlist[i][0];
+ return -1;
+ }).filter(function(id){return id!=-1;});
+ socket.emit("createroom",users);
+}
+
+function dodestroyroom(roomid){
+ socket.emit("roomdestroy",roomid);
+}
+
+function drawboard(ctx,cellsz,bd,clr){
+ var x,y,i,angle,radius;
+ ctx.clearRect(0,0,W*cellsz+1,H*cellsz+1);
+ ctx.strokeStyle=COLOURS[+clr];
+ ctx.beginPath();
+ for(y=0;y<H;y++){
+ for(x=0;x<W;x++){
+ ctx.moveTo(x*cellsz+.5,y*cellsz+.5);
+ ctx.lineTo((x+1)*cellsz+.5,y*cellsz+.5);
+ ctx.lineTo((x+1)*cellsz+.5,(y+1)*cellsz+.5);
+ ctx.lineTo(x*cellsz+.5,(y+1)*cellsz+.5);
+ ctx.lineTo(x*cellsz+.5,y*cellsz+.5);
+ }
+ }
+ 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:.15;
+ for(i=0;i<bd[y][x].n;i++){
+ ctx.beginPath();
+ ctx.arc(
+ (x+.5+radius*Math.cos(angle*i))*cellsz,
+ (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)]);
+ if(checkwin(bd)!=-1)break;
+ } while(stage.length);
+ return anims;
+}
+
+var applymove_queue={}; //objects of room ids
+var applymove_busy={};
+
+function applymove(ctx,cellsz,idx,c,aqid){
+ var bd=rooms[aqid].bd,onturn=rooms[aqid].onturn;
+ applymove_busy[aqid]=true;
+ bd[~~(idx/W)][idx%W].n++;
+ bd[~~(idx/W)][idx%W].c=c;
+ drawboard(ctx,cellsz,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(ctx,cellsz,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(
+ (fx*(1-time/10)+tx*time/10+.5)*cellsz,
+ (fy*(1-time/10)+ty*time/10+.5)*cellsz,
+ cellsz*.2,0,2*Math.PI,1);
+ ctx.fill();
+ }
+ if(time==10){
+ clearInterval(interval);
+ bd=rooms[aqid].bd=newbd;
+ drawboard(ctx,cellsz,bd,onturn);
+ if(finalstage){
+ applymove_busy[aqid]=false;
+ while(applymove_queue[aqid].length&&typeof applymove_queue[aqid][0]=="function"){
+ applymove_queue[aqid][0]();
+ applymove_queue[aqid].shift();
+ }
+ if(applymove_queue[aqid].length){
+ setTimeout(function(){
+ applymove.apply(null,applymove_queue[aqid][0]);
+ applymove_queue[aqid].shift();
+ },50);
+ }
+ }
+ }
+ time++;
+ },20);
+ },350*i+1);
+ });
+}
+
+function queueapplymove(id,ctx,cellsz,idx,c){
+ if(!applymove_queue[id])applymove_queue[id]=[];
+ if(!applymove_busy[id])applymove_busy[id]=false;
+ if(applymove_busy[id]){
+ if(typeof ctx=="function")applymove_queue[id].push(ctx);
+ else applymove_queue[id].push([ctx,cellsz,idx,c,id]);
+ } else {
+ if(typeof ctx=="function")ctx();
+ else applymove(ctx,cellsz,idx,c,id);
+ }
+}
+</script>
+<style>
+body{
+ background-color:#fff;
+ margin:0;
+ font-family:Monaco,Menlo,"Courier New",Monospace;
+ font-size:14px;
+}
+header{
+ background-color:#fee;
+}
+header h1{
+ margin-top:0;
+ margin-bottom:0;
+ padding:10px;
+ padding-left:20px;
+ width:350px;
+}
+div#headerbottom{
+ height:10px;
+ background: #fee;
+ background: -moz-linear-gradient(top, #fee 0%, #fff 100%);
+ background: -webkit-gradient(left top, left bottom, color-stop(0%, #fee), color-stop(100%, #fff));
+ background: -webkit-linear-gradient(top, #fee 0%, #fff 100%);
+ background: -o-linear-gradient(top, #fee 0%, #fff 100%);
+ background: -ms-linear-gradient(top, #fee 0%, #fff 100%);
+ background: linear-gradient(to bottom, #fee 0%, #fff 100%);
+}
+
+div#nickbar{
+ float:right;
+ margin-top:15px;
+ font-size:10px;
+}
+span#nickname{
+ font-weight:bold;
+}
+
+
+div#body{
+ margin:5px;
+}
+
+div#userlistcontainer{
+ width:190px;
+}
+div#userlist{
+ width:150px;
+ height:200px;
+ border:1px #eee solid;
+ overflow-y:scroll;
+ -moz-user-select:none;
+ -webkit-user-select:none;
+ -ms-user-select:none;
+}
+div#userlist > div{
+ cursor:pointer;
+ padding:1px;
+}
+div#userlist > div:hover{
+ background-color:#edd;
+}
+div#userlist > div.selected{
+ background-color:#dcc;
+ border:1px #abb solid;
+ padding:0;
+}
+
+input#roomuserinput{
+ width:180px;
+}
+
+
+div.room{
+ background-color:#edd;
+ -webkit-box-shadow:0px 0px 2px 3px #edd;
+ -moz-box-shadow:0px 0px 2px 3px #edd;
+ box-shadow:0px 0px 2px 3px #edd;
+ padding:10px;
+ margin:10px;
+ width:300px;
+ cursor:pointer;
+}
+
+div.roomui{
+ position:absolute;
+ top:50%;
+ margin-top:-300px;
+ left:50%;
+ margin-left:-350px;
+ width:700px;
+ height:600px;
+ padding:10px;
+ background-color:#edd;
+ border-radius:5px;
+}
+
+div.chatdiv{
+ float:right;
+ width:200px;
+ height:200px;
+}
+table.chatlogtable{
+ display:block;
+ width:100%;
+ height:160px;
+ border:1px black solid;
+ overflow-y:scroll;
+}
+div.chatitem{
+ width:200px;
+ word-wrap:break-word;
+}
+input.chatinput{
+ width:200px;
+}
+
+div.gamestatusdiv{
+ font-style:italic;
+ font-weight:bold;
+ font-size:12px;
+}
+
+
+.invisible{
+ display:none;
+}
+
+
+div#roomuisplash{
+ position:absolute;
+ left:0;
+ top:0;
+ width:100%;
+ height:100%;
+ background-color:rgba(0,0,0,0.4);
+}
+</style>
+</head>
+<body>
+<div id="roomuisplash" class="invisible" onclick="visibleroomuidiv.classList.add('invisible');visibleroomuidiv=null;event.target.classList.add('invisible')"></div>
+<header>
+ <div id="nickbar">Nickname: <span id="nickname"></span> <input type="button" onclick="changenick()" value="Change"></div>
+ <h1>Multichain</h1>
+ <div id="headerbottom"></div>
+</header>
+<div id="body">
+ <div id="createroomcontainer" style="float:right">
+ <b>Online users:</b>
+ <div id="userlist"></div>
+ <input type="text" id="roomuserinput" disabled> <br>
+ <input type="button" onclick="createroom()" value="Create room">
+ </div>
+ <div id="roomlistcontainer"></div>
+</div>
+</body>
+</html>
diff --git a/index.js b/index.js
new file mode 100755
index 0000000..d2432ac
--- /dev/null
+++ b/index.js
@@ -0,0 +1,248 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var app=require("express")(),
+ http=require("http").Server(app),
+ io=require("socket.io")(http),
+ fs=require("fs"),
+ spawn=require("child_process").spawn,
+ mkdirp=require("mkdirp"),
+ C=require("./common.js");
+
+
+var HTTPPORT=8090;
+
+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);
+ });
+});
+
+function Game(_np){
+ if(!(this instanceof Game))return new Game(_np);
+ this.bd=emptyboard();
+ this.np=np;
+ this.onturn=0;
+}
+Game.prototype.applymove=function(player,pos){
+ if(this.onturn!=player)return false;
+ var x=pos%C.W,y=~~(pos/C.W);
+ if(this.bd[y][x].n&&this.bd[y][x].c!=player)return false;
+ this.bd[y][x].c=player;
+ this.bd[y][x].n++;
+ this.bd=C.stabilise(this.bd);
+ do this.onturn=(this.onturn+1)%this.np;
+ while C.countballs(this.bd,this.onturn)==0;
+ return true;
+};
+Game.prototype.checkwin=function(){
+ if(C.countballs(this.bd)<this.np)return -1;
+ return C.checkwin(this.bd);
+};
+
+var connlist=[]; //sorted on id, because uniqid is strictly increasing
+var rooms={}; //{<roomid>: {ids:[id's], game:Game}}
+
+function findid(id){
+ var lo=0,mid,hi=connlist.length-1;
+ if(connlist[lo].id==id)return lo;
+ if(connlist[hi].id==id)return hi;
+ while(lo<hi){
+ mid=lo+(hi-lo)/2|0;
+ if(connlist[mid].id==id)return mid;
+ if(connlist[mid].id<id)lo=mid+1;
+ else hi=mid-1;
+ }
+ if(hi<lo||connlist[lo].id!=id)return -1; //not found
+ return lo;
+}
+
+function findname(name){
+ var i;
+ for(i=0;i<connlist.length;i++)if(connlist[i].name==name)return i;
+ return -1;
+}
+
+io.on("connection",function(conn){
+ var obj={
+ id:uniqid(),
+ name:"user",
+ conn:conn,
+ rooms:[],
+ pending:[]
+ };
+ obj.name+=obj.id;
+ connlist.push(obj);
+ conn.emit("me",[obj.id,obj.name]);
+ io.emit("userlist",connlist.map(function(o){return [o.id,o.name];}));
+
+ conn.on("disconnect",function(){
+ var idx=findid(obj.id);
+ if(idx==-1)return; //WAT
+ obj.pending.forEach(function(pendid){
+ io.to(pendid).emit("roomdestroy",pendid);
+ rooms[pendid].ids.forEach(function(id){
+ var o=connlist[findid(id)];
+ o.conn.leave(pendid);
+ o.rooms.splice(o.rooms.indexOf(pendid),1);
+ });
+ delete rooms[pendid];
+ });
+ obj.rooms.forEach(function(roomid){
+ io.to(roomid).emit("roomdestroy",roomid);
+ rooms[roomid].ids.forEach(function(id){
+ var o=connlist[findid(id)];
+ o.conn.leave(roomid);
+ o.rooms.splice(o.rooms.indexOf(roomid),1);
+ });
+ delete rooms[roomid];
+ });
+ connlist.splice(idx,1);
+ io.emit("userlist",connlist.map(function(o){return [o.id,o.name];}));
+ });
+ conn.on("me",function(cb){
+ cb([obj.id,obj.name]);
+ });
+ conn.on("setnick",function(nick){
+ var i;
+ if(C.validatenick(nick)){
+ if(findname(nick)!=-1){
+ i=1;
+ while(findname(nick+"_"+i)!=-1)i++;
+ nick+="_"+i;
+ }
+ obj.name=nick;
+ conn.emit("me",[obj.id,obj.name]);
+ io.emit("userlist",connlist.map(function(o){return [o.id,o.name];}));
+ } else conn.emit("message","That's an invalid nick!");
+ });
+ conn.on("userlist",function(cb){
+ cb(connlist.map(function(o){return [o.id,o.name];}));
+ });
+ conn.on("rooms",function(cb){
+ cb(obj.rooms);
+ });
+ conn.on("createroom",function(ids){ //give all id's, except your own
+ var myidx=findid(obj.id);
+ var indices=ids.map(function(id){return findid(id);}).filter(function(id){return id!=-1;});
+ if(indices.length==0){
+ conn.emit("message","Room too small; can only create rooms with 2 or more people");
+ return;
+ }
+ var roomid="room_"+uniqid();
+ rooms[roomid]={ids:[obj.id],game:null};
+ connlist[myidx].rooms.push(roomid);
+ conn.emit("room",roomid,[[obj.id,obj.name]]);
+ conn.join(roomid);
+ indices.forEach(function(idx){
+ connlist[idx].conn.emit("roominvite",roomid,[obj.id,obj.name]);
+ connlist[idx].pending.push(roomid);
+ });
+ });
+ conn.on("inviteaccept",function(roomid){
+ var idx=obj.pending.indexOf(roomid);
+ if(idx!=-1)obj.pending.splice(idx,1);
+ if(idx==-1||rooms[roomid]==undefined){
+ conn.emit("message","Cannot accept non-existent invitation");
+ return;
+ }
+ obj.rooms.push(roomid);
+ conn.emit("room",roomid,rooms[roomid].ids.map(function(id){
+ return [id,connlist[findid(id)].name];
+ }));
+ conn.join(roomid);
+ io.to(roomid).emit("roomjoin",roomid,[obj.id,obj.name]);
+ rooms[roomid].ids.push(obj.id);
+ });
+ function guardroomid(roomid){
+ if(obj.rooms.indexOf(roomid)==-1){
+ conn.emit("message","No member of that room!");
+ return false;
+ }
+ return true;
+ }
+ conn.on("roomdestroy",function(roomid){
+ if(!guardroomid(roomid))return;
+ io.to(roomid).emit("roomdestroy",roomid);
+ rooms[roomid].ids.forEach(function(id){
+ var o=connlist[findid(id)];
+ o.conn.leave(roomid);
+ o.rooms.splice(o.rooms.indexOf(roomid),1);
+ });
+ delete rooms[roomid];
+ });
+ conn.on("roomusers",function(roomid,cb){
+ if(!guardroomid(roomid)){
+ cb(false);
+ return;
+ }
+ cb(rooms[roomid].ids.map(function(id){
+ return [id,connlist[findid(id)].name];
+ }));
+ });
+ conn.on("roomchat",function(roomid,message){
+ if(!guardroomid(roomid))return;
+ io.to(roomid).emit("roomchat",roomid,[obj.id,obj.name],message);
+ });
+ conn.on("roomcreategame",function(roomid){
+ if(!guardroomid(roomid))return;
+ if(rooms[roomid].game!=null){
+ conn.emit("message","Room already has a running game");
+ return;
+ }
+ rooms[roomid].game=new Game(rooms[roomid].ids.length);
+ io.to(roomid).emit("roomcreategame",roomid);
+ connlist[findid(rooms[roomid].ids[0])].conn.emit("roomgameturn",roomid,0);
+ });
+ conn.on("roomhasgame",function(roomid,cb){
+ if(!guardroomid(roomid))return;
+ cb(rooms[roomid].game!=null);
+ });
+ function guardroomgame(roomid){
+ if(rooms[roomid].game==null){
+ conn.emit("message","No game in that room!");
+ return false;
+ }
+ return true;
+ }
+ conn.on("roomgameusers",function(roomid,cb){
+ if(!guardroomid(roomid)||!guardroomgame(roomid)){
+ cb(false);
+ return;
+ }
+ cb(rooms[roomid].ids.map(function(id){
+ return [id,connlist[findid(id)].name];
+ }));
+ });
+ conn.on("roomgameboard",function(roomid,cb){
+ if(!guardroomid(roomid)||!guardroomgame(roomid)){
+ cb(false);
+ return;
+ }
+ cb(rooms[roomid].game.bd);
+ });
+ conn.on("roomgameplace",function(roomid,pos){
+ if(!guardroomid(roomid)||!guardroomgame(roomid))return;
+ var playeridx=rooms[roomid].ids.indexOf(obj.id);
+ if(!rooms[roomid].game.applymove(pos,playeridx)){
+ conn.emit("message","Invalid move!");
+ return;
+ }
+ io.to(roomid).emit("roomgameplace",roomid,pos,playeridx);
+ connlist[findid(rooms[roomid].ids[rooms[roomid].game.onturn])].conn.emit("roomgameturn",roomid,rooms[roomid].game.onturn);
+ });
+});
+
+http.listen(HTTPPORT,function(){
+ console.log("Listening on http://localhost:"+HTTPPORT);
+});
diff --git a/interactorindex.html b/interactorindex.html
new file mode 100644
index 0000000..4593974
--- /dev/null
+++ b/interactorindex.html
@@ -0,0 +1,228 @@
+<!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:.15;
+ 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)]);
+ if(checkwin(bd)!=-1)break;
+ } 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;
+ while(applymove_queue.length&&typeof applymove_queue[0]=="function"){
+ applymove_queue[0]();
+ applymove_queue.shift();
+ }
+ if(applymove_queue.length){
+ setTimeout(function(){
+ applymove(applymove_queue[0][0],applymove_queue[0][1]);
+ applymove_queue.shift();
+ },50);
+ }
+ }
+ }
+ time++;
+ },20);
+ },350*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){
+ if(applymove_busy)applymove_queue.push(function(){
+ onturn=player;
+ drawboard(bd,onturn);
+ });
+ else {
+ onturn=player;
+ drawboard(bd,onturn);
+ }
+});
+socket.on("getusermove",function(){
+ if(applymove_busy)applymove_queue.push(getusermove);
+ else getusermove();
+});
+socket.on("win",function(player){
+ setstatustext("<b><i>Player "+(player+1)+" won!</i></b>");
+});
+</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/package.json b/package.json
new file mode 100644
index 0000000..8a258d0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "multichain",
+ "version": "1.0.0",
+ "description": "Multiplayer chain reaction",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [
+ "chain",
+ "reaction",
+ "chainreaction",
+ "multiplayer",
+ "game",
+ "server"
+ ],
+ "author": "Tom Smeding <hallo@tomsmeding.nl> (http://tomsmeding.com)",
+ "license": "MIT",
+ "dependencies": {
+ "express": "^4.13.3",
+ "mkdirp": "^0.5.1",
+ "socket.io": "^1.3.7"
+ }
+}