diff options
author | tomsmeding <hallo@tomsmeding.nl> | 2015-12-05 20:40:32 +0100 |
---|---|---|
committer | tomsmeding <hallo@tomsmeding.nl> | 2015-12-05 20:40:32 +0100 |
commit | 1954ed67ef61c509560418a9d2da58083b822996 (patch) | |
tree | 870237504df86fcdd6cfaab2948e73e7211cf95d |
Initial enzo
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | common.js | 82 | ||||
-rw-r--r-- | index.html | 545 | ||||
-rwxr-xr-x | index.js | 248 | ||||
-rw-r--r-- | interactorindex.html | 228 | ||||
-rw-r--r-- | package.json | 24 |
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" + } +} |