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" +  } +}  | 
