<!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.reload(); }); 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("roomcreategame",roomcreategame) socket.on("roomgameturn",roomgameturn); socket.on("roomgameplace",roomgameplace); socket.on("roomgamewin",roomgamewin); 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=null; this.onturn=0; this.cellsz=40; this.canplace=false; this.lastplacepos=-1; 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; this.becomeVisible(); }.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.classList.add("uiclosebtn"); 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"); e.addEventListener("click",function(){ this.chatinput.focus(); }.bind(this)); table=document.createElement("table"); table.classList.add("chatlogtable"); tbody=document.createElement("tbody"); tbody.classList.add("chatlogtbody"); table.appendChild(tbody); e.appendChild(table); this.chatinput=document.createElement("input"); this.chatinput.type="text"; this.chatinput.classList.add("chatinput"); this.chatinput.setAttribute("placeholder","Type something..."); this.chatinput.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(this.chatinput); this.uidiv.appendChild(e); this.gamediv=document.createElement("div"); this.gamediv.classList.add("gamediv"); this.gamediv.classList.add("invisible"); //no game running yet 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){ this.chatinput.focus(); if(!this.canplace)return; if(ev.target!=this.cvs)return; var bbox=ev.target.getBoundingClientRect(); var x=~~((ev.clientX-bbox.left)/this.cellsz), y=~~((ev.clientY-bbox.top)/this.cellsz), idx=W*y+x; if(x<0||y<0||x>=W||y>=H)return; socket.emit("roomgameplace",this.id,idx); }.bind(this)); //drawboard(this.ctx,this.cellsz,this.bd,0,this.lastplacepos); this.gamediv.appendChild(this.cvs); this.uidiv.appendChild(this.gamediv); this.newgamebtn=document.createElement("input"); this.newgamebtn.type="button"; this.newgamebtn.value="Start new game!"; this.newgamebtn.addEventListener("click",function(){ socket.emit("roomcreategame",this.id); //just a request }.bind(this)); this.uidiv.appendChild(this.newgamebtn); this.statusdiv=document.createElement("div"); this.statusdiv.classList.add("gamestatusdiv"); this.uidiv.appendChild(this.statusdiv); document.body.appendChild(this.uidiv); setTimeout(this.becomeVisible.bind(this),1); } Room.prototype.becomeVisible=function(){ if(visibleroomuidiv)visibleroomuidiv.classList.add("invisible"); else document.getElementById("roomuisplash").classList.remove("invisible"); visibleroomuidiv=this.uidiv; this.uidiv.classList.remove("invisible"); this.chatinput.focus(); }; 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,isme){ 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(); if(!isme)sounddong(); }; Room.prototype.creategame=function(){ this.newgamebtn.classList.add("invisible"); this.gamediv.classList.remove("invisible"); this.bd=emptyboard(); drawboard(this.ctx,this.cellsz,this.bd,0,this.lastplacepos); this.statusdiv.innerHTML="Waiting for player 1..."; }; Room.prototype.myturn=function(ot){ this.canplace=true; this.onturn=ot; this.statusdiv.innerHTML="Your turn!"; }; Room.prototype.place=function(pos,pidx,isme){ //console.log("place: onturn "+this.onturn+" -> "+pidx); this.onturn=pidx; this.canplace=false; this.lastplacepos=pos; queueapplymove(this.id,this.ctx,this.cellsz,pos,pidx,this.lastplacepos); queueapplymove(this.id,function(){ //console.log("place qam_func: onturn "+this.onturn+" -> "+(this.onturn+1)%this.userlist.length); if(!this.canplace){ this.onturn=(this.onturn+1)%this.userlist.length; this.statusdiv.innerHTML="Waiting for player "+(this.onturn+1)+"..."; } drawboard(this.ctx,this.cellsz,this.bd,this.onturn,this.lastplacepos); }.bind(this)); if(!isme)sounddong(); }; Room.prototype.gotwin=function(pidx){ queueapplymove(this.id,function(){ this.onturn=pidx; this.canplace=false; drawboard(this.ctx,this.cellsz,this.bd,this.onturn,this.lastplacepos); this.statusdiv.innerHTML="Player "+(this.onturn+1)+" won!"; this.newgamebtn.classList.remove("invisible"); }.bind(this)); }; function roominvite(roomid,by){ if(!confirm("User '"+by[1]+"' sent you an invite to join their room. Accept?")){ socket.emit("inviteaccept",roomid,true); return; } socket.emit("inviteaccept",roomid); } function roomdestroy(roomid){ rooms[roomid].destroy(); delete rooms[roomid]; } function roomchat(roomid,by,msg,isme){ rooms[roomid].chat(by,msg,isme); } function roomcreategame(roomid){ rooms[roomid].creategame(); } function roomgameturn(roomid,ot){ rooms[roomid].myturn(ot); } function roomgameplace(roomid,pos,pidx,isme){ rooms[roomid].place(pos,pidx,isme); } function roomgamewin(roomid,pidx){ rooms[roomid].gotwin(pidx); } 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,lastplacepos){ 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(); if(lastplacepos!=-1){ x=lastplacepos%W; y=~~(lastplacepos/W); ctx.fillStyle="rgba(0,0,0,0.2)"; ctx.fillRect(x*cellsz,y*cellsz,cellsz,cellsz); } 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,lastplacepos,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,lastplacepos); var anims=stabiliseanims(bd); console.log(anims); anims.forEach(function(pair,i){ var stage=pair[0],newbd=pair[1]; var finalstage=i==anims.length-1; setTimeout(function(){ console.log("anim stage",stage); 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,lastplacepos); 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,lastplacepos); 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,lastplacepos){ 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,lastplacepos,id]); } else { if(typeof ctx=="function")ctx(); else applymove(ctx,cellsz,idx,c,lastplacepos,id); } } var sounddong__audioelem=null; function sounddong(){ if(sounddong__audioelem==null)sounddong__audioelem=document.getElementById("dongaudioelem"); sounddong__audioelem.currentTime=0; sounddong__audioelem.play(); } </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.uiclosebtn{ float:right; margin:5px; margin-left:10px; cursor:pointer; } div.chatdiv{ float:right; width:300px; height:600px; } table.chatlogtable{ display:block; width:100%; height:560px; border:1px black solid; overflow-y:scroll; } div.chatitem{ width:300px; word-wrap:break-word; } input.chatinput{ width:300px; } 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> <audio id="dongaudioelem" preload="auto"> <source src="dong.ogg"> <source src="dong.mp3"> </audio> </body> </html>