/*
  PacEngine - an object oriented Pac-Man engine
  (c) N.Landsteiner 2006; www.masswerk.at
  based on JavaScript-PacMan (c) N.Landsteiner 1997
  all rights reserved
 */


function PacEngine() {
	this.initialize();
}

PacEngine.prototype= {
	gHomePos: new Array( [7,11], [7,10], [6,11], [6,10] ),
	gColor: new Array( '','red', 'pink', 'green', 'orange' ),
	ghostBonus: new Array(200,400,800,1600),
	gbid: new Array(1,2,4,8),
	pillPeriode: 18,
	bonusLifeScore: 10000,
	
	Ghost: function() {
		this.r= 0;
		this.c= 0;
		this.s= 0;
		this.d= 0;
		this.z= 0;
		this.lastn=null;
		this.bid=0;
		this.bm=0;
	},
	Pacman: function() {
		this.r= 0;
		this.c= 0;
		this.dir= 0;
		this.md= 0;
		this.dx= 0;
		this.dy= 0;
	},
	levels: [
		[
			'####################',
			'#........##........#',
			'#*##.###.##.###.##*#',
			'#..................#',
			'####.##.####.##.####',
			'####.##.#  #.##.####',
			'    ....#  =....    ',
			'####.##.####.##.####',
			'####..... ......####',
			'#....##.####.##....#',
			'#*#.###..##..###.#*#',
			'#.#.####.##.####.#.#',
			'#..................#',
			'####################'
		],
		[
			'####################',
			'#.....#......#.....#',
			'#*###.#.####.#.###*#',
			'#..................#',
			'###.#.#.####.#.#.###',
			'   .#.#.#  #.#.#.   ',
			'###.#...#  =...#.###',
			'###.###.####.###.###',
			'#........ .........#',
			'#.##.###.##.###.##.#',
			'#.##.#...##...#.##.#',
			'#*##.#.######.#.##*#',
			'#..................#',
			'####################'
		],
		[
			'####################',
			'      #......#      ',
			'##### #.####.# #####',
			'#*................*#',
			'#.#####.####.#####.#',
			'#..#....#  #....#..#',
			'##...##.#  =.##...##',
			'##.####.####.####.##',
			'  ...#... ....#...  ',
			'##.#.#.######.#.#.##',
			'#*.#.....##.....#.*#',
			'#.##.###.##.###.##.#',
			'#..................#',
			'####################'
		],
		[
			'####################',
			'#......#....#......#',
			'#*####.#.##.#.####*#',
			'#..................#',
			'###.#.#.####.#.#.###',
			' ...#...#  #...#... ',
			'#.#...#.#  =.#...#.#',
			'#.#.###.####.###.#.#',
			'#........ .........#',
			'##.##.##.##.##.##.##',
			'#*....#..##..#....*#',
			'#.###.#.####.#.###.#',
			'#.....#......#.....#',
			'####################'
		],
		[
			'####################',
			'#..................#',
			'#*#.#.########.#.#*#',
			'#.#...#......#...#.#',
			'#...#...####...#...#',
			'###.###.#  #.###.###',
			'   .#  .#  =.  #.   ',
			'###...#.####.#...###',
			'   .#.... .....#.   ',
			'###.#.#.####.#.#.###',
			'#.....#......#.....#',
			'#*##.###.##.###.##*#',
			'#........##........#',
			'####################'
		],
		[
			'####### #### #######',
			'#*....#......#....*#',
			'#.###...####...###.#',
			'#.....#......#.....#',
			'#.#.#.#.####.#.#.#.#',
			'#.#.#.#.#  #.#.#.#.#',
			' .#...#.#  =.#...#. ',
			'#.###.#.####.#.###.#',
			'#........ .........#',
			'##.##.##.##.##.##.##',
			'#...#.#......#.#...#',
			'#.#.#.#.####.#.#.#.#',
			'#*................*#',
			'####### #### #######'
		],
		[
			'##### ######## #####',
			'#.*..............*.#',
			'#.#.#.##.##.##.#.#.#',
			'#...#..........#...#',
			'###.###.####.###.###',
			'#.......#  #.......#',
			' .###.#.#  =.#.###. ',
			'#.....#.####.#.....#',
			'####.##.. ...##.####',
			'#.....#.#.##.#.....#',
			'#.###...#..#...###.#',
			'#.###.#.##.#.#.###.#',
			'#.*...#......#...*.#',
			'##### ######## #####'
		],
		[
			'### ############ ###',
			'#*....#......#....*#',
			'#.###...####...###.#',
			' .##..#......#..##. ',
			'#..#.##.####.##.#..#',
			'##...##.#  #.##...##',
			'#..#..#.#  =.#..#..#',
			'#.###...####...###.#',
			'#.....#.. ...#.....#',
			'#.#####.####.#####.#',
			' ....#........#.... ',
			'#.##...#.##.#...##.#',
			'#*...#........#...*#',
			'### ############ ###'
		],
		[
			'# ### #### ## ### ##',
			'#.##*....#.........#',
			'#.....##.#.###.###*#',
			'#.###..........###.#',
			'#.....#.######.###.#',
			' .###.#.#  #....... ',
			'#.###.#.#  =.##.##.#',
			'#.###.#.####.#...#.#',
			' ........ ...#.#... ',
			'#.###.####.#...#.#.#',
			'#...#......#.#..*#.#',
			'#.#...#.##.#.##.##.#',
			'#.##*.#............#',
			'# ### #### ## ### ##'
		],
		[
			'##### ######## # ###',
			'#.*................#',
			'#.###.####.###.###.#',
			'#.###.#......#.#...#',
			'#.....#.####.#.#*#.#',
			' .###.#.#  #.....#. ',
			'#.#.....#  =.#.###.#',
			'#.#.###.####.#.....#',
			' ...###.. .....#.#. ',
			'#.#.###.######.#.#.#',
			'#.#*.......#...#...#',
			'#.###.####.#.#.###.#',
			'#..............#...#',
			'##### ######## # ###'
		]
	],
	initialize: function() {
		this.pac=new this.Pacman();
		this.g=new Array();
		for (var i=1; i<=4; i++) {
			this.g[i]=new this.Ghost();
			this.g[i].bid=this.gbid[i-1];
			this.g[i].bm=15^this.gbid[i-1];
		}
		this.f1= new Array();
		this.f2= new Array();
		this.fw= new Array();
		this.fft= new Array();
		this.map= new Array();
		this.mapMask= new Array();
		for (var i=1; i<=14; i++) {
			this.f1[i]=new Array();
			this.f2[i]=new Array();
			this.fw[i]=new Array();
			this.fft[i]=new Array();
			this.map[i]=new Array();
			this.mapMask[i]=new Array();
		}
		
		this.tx= new Array();
		this.ty= new Array();
		this.t1= new Array();
		this.t1[0]=0;
		this.t1[1]= 9; // rd
		this.t1[2]=10; // ld
		this.t1[3]= 5; // ru
		this.t1[4]= 6; // lu
		this.t1[5]=13; // rdu
		this.t1[6]=14; // ldu
		this.t1[7]=11; // rld
		this.t1[8]= 7; // rlu
		this.t1[9]=15; // rlud
		this.tx[0]=0;  this.ty[0]=0;
		this.tx[1]=1;  this.ty[1]=0;  //r
		this.tx[2]=-1; this.ty[2]=0;  //l
		this.tx[4]=0;  this.ty[4]=-1; //u
		this.tx[8]=0;  this.ty[8]=1;  //d
		
		this.t2 = new Array();
		this.t2[0]= [0];
		this.t2[1]= [1];
		this.t2[2]= [2];
		this.t2[4]= [4];
		this.t2[8]= [8];
		this.t2[3]= [1, 2];
		this.t2[9]= [1, 8];
		this.t2[10]=[2, 8];
		this.t2[12]=[4, 8];
		this.t2[5]= [1, 4];
		this.t2[6]= [2, 4];
		this.t2[7]= [1, 2, 4];
		this.t2[11]=[1, 2, 8];
		this.t2[13]=[1, 4, 8];
		this.t2[14]=[2, 4, 8];
		this.t2[15]=[1, 2, 4, 8];
		
		this.tdx=[2, 0, 1];
		this.tdy=[4, 0, 8];
		
		this.t3 = new Array();
		this.t3[0]=0; this.t3[1]=2; this.t3[2]=1; this.t3[4]=8; this.t3[8]=4;
		
		this.nextNodes= new Array();
		this.pill= false;
		this.pillCnt= 0;
		this.food= 0;
		this.nLevel= 0;
		this.nLife=0;
		this.gStep;
		this.bonusCnt=0;
		this.bonusLifeCnt=0;
		this.pacInitDir=0;
		this.ghostInitDir=0;
		this.pGstrat=0;
		this.pGsLookahead=0;
		this.gameStatus=0;
		this.pacNext=null;
		this.ghostCnt=0;
		this.report=new Array();
		this.bonusReport=new Array();
		this.pacDir=1;
	},
	
	buildMaze: function(m) {
		var r, c, d, f1r, fwr, z;
		this.food=0;
		for (r=1; r<=14; r++) {
			fwr=this.fw[r];
			f1r=this.f1[r];
			for (c=1; c<=20; c++) {
				f1r[c]=0;
				z=m[r-1].charAt(c-1);
				if (z=='.') {
					f1r[c]=9;
					this.food++;
					this.map[r][c]=4;
				}
				else if (z=='*') {
					f1r[c]=8;
					this.food++;
					this.map[r][c]=5;
				}
				else {
					if (z=='#') {
						this.map[r][c]=1;
					}
					else if (z=='=') {
						this.map[r][c]=2;
					}
					else {
						this.map[r][c]=0;
					}
				}
				fwr[c]=(z!='#' && z!='=')? true:false;
				this.mapMask[r][c]=false;
			}
		}
		this.pacInitDir=0;
		if (this.fw[9][9]) this.pacInitDir|=2;
		if (this.fw[9][11]) this.pacInitDir|=1;
		if (this.fw[10][10]) this.pacInitDir|=8;
		this.ghostInitDir=0;
		if (this.fw[6][13]) this.ghostInitDir|=4;
		if (this.fw[8][13]) this.ghostInitDir|=8;
		if (this.fw[7][14]) this.ghostInitDir|=1;
		
		// calc directions
		this.f2=new Array();
		for (var r=1; r<=14; r++) {
			this.f2[r]=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
		}
		for (var r=2; r<14; r++) {
			for (var c=2; c<20; c++) {
				if ((this.fw[r][c]) && ((r<6) || (r>7) || (c<10) || (c>11)))  {
					d=0;
					if (this.fw[r-1][c]) d|=4;
					if (this.fw[r+1][c]) d|=8;
					if (this.fw[r][c-1]) d|=2;
					if (this.fw[r][c+1]) d|=1;
					if ((d==3) || (d==12)) d=0;
					this.f2[r][c]=d;
				}
			}
		}
		for (var r=5; r<9; r++) {
			for (var c=9; c<13; c++) {
				this.mapMask[r][c]=true;
			}
		}
	},
	calcNextNodes: function() {
		var dirs=this.t2[15];
		var fm,fr,fd,dx,dy,x,y;
		for (var m=0; m<dirs.length; m++) {
			var d=dirs[m];
			fd=this.nextNodes[d]=new Array();
			for (var r=1; r<=14; r++) {
				fr=fd[r]=new Array();
				for (var c=1; c<=20; c++) {
					if (this.f2[r][c] & d) {
						dx=this.tx[d];
						dy=this.ty[d];
						y=r;
						x=c;
						while (true) {
							if (y==1) {fr[c]=[13,c]; break;}
							if (y==14) {fr[c]=[2,c]; break;}
							if (x==1) {fr[c]=[r,19]; break;}
							if (x==20) {fr[c]=[r,2]; break;}
							x+=dx;
							y+=dy;
							if (this.f2[y][x]) {fr[c]=[y,x]; break;}
						}
					}
					else {
						fr[c]=null;
					}
				}
			}
		}
		this.nextNodes[1][7][12]=[7,13];
	},
	
	pHome: function() {
		this.pill=false; this.pillCnt=0;
		this.movedir=0;
		this.pacNext=null;
		this.pacDir= 1;
		with (this.pac) {
			r=9; c=10;
			md=0; dir= this.pacInitDir;
			dx=0; dy=0;
			pn=1; p=1;
		}
	},
	gHome: function(i) {
		with (this.g[i]) {
			for (var k=0; k<this.gHomePos.length; k++) {
				if (this.tileFree(i,this.gHomePos[k][0],this.gHomePos[k][1])) {
					r=this.gHomePos[k][0];
					c=this.gHomePos[k][1];
					break
				}
			};
			s=0;
			z=0;
			d=0;
			this.setGhost(i,r,c);
		}
	},
	
	newGame: function() {
		this.report.length=0;
		this.nLife=3;
		this.nScore=0;
		this.nLevel=0;
		this.bonusLifeCnt=0;
		this.startLevel();
	},
	startLevel: function() {
		this.nLevel++;
		this.gameStatus=0;
		//this.pGstrat=Math.min(0.7, 0.2+this.nLevel/10);
		//this.pGsLookahead=Math.min(0.5, 0.1+this.nLevel/10);
		this.pGstrat=Math.min(0.7, 0.4+this.nLevel/10);
		this.pGsLookahead=Math.min(0.5, 0.3+this.nLevel/10);
		this.buildMaze(this.levels[(this.nLevel-1)%this.levels.length]);
		this.calcNextNodes();
		for (var r=1; r<=14; r++) {
			for (var c=1; c<=20; c++) {
				this.fft[r][c]=0;
			}
		}
		for (var i=1; i<=4; i++) this.gHome(i);
		this.pHome();
		this.updateMapMask();
	},
	
	getRand: function(x) {
		return Math.floor(Math.random() * x);
	},
	trunkBy: function(v,m) {
		v%=m;
		if (v==0) v=m;
		return v;
	},
	tileFree: function(n, r, c) {
		if (n) {
			return (this.fft[r][c]&this.g[n].bm)? false:true;
		}
		else {
			return (this.fft[r][c])? false:true;
		}
	},
	addScore: function(n) {
		this.nScore+=n;
		this.bonusLifeCnt+=n;
		if (this.bonusLifeCnt>=this.bonusLifeScore) {
			this.bonusLife();
			this.bonusLifeCnt-=this.bonusLifeScore;
		}
	},
	
	gameStep: function() {
		this.report.length=0;
		this.bonusReport.length=0;
		if (this.gameStatus==0) this.doMove()
		else if (this.gameStatus==1) this.endLevel()
		else if (this.gameStatus==2) this.startLevel();
		if (this.gameStatus==1) this.report.push('newlevel');
	},
	
	doMove: function() {
		this.pMove();
		if (this.pac.md) {
			with (this.pac) {
				c=this.trunkBy(c+dx,20);
				r=this.trunkBy(r+dy,14);
				this.updateMapMask();
				if (this.f2[r][c]) dir=this.f2[r][c];
			}
			if (this.testCrash()) return;
			var r=this.pac.r;
			var c=this.pac.c;
			if (this.f1[r][c]>=8) {
				if (this.f1[r][c]==8) {
					if (this.pill==false) {
						this.pill=true;
					};
					this.pillCnt=this.pillPeriode;
					this.addScore(40);
					this.ghostCnt=0;
					this.report.push('pill');
				}
				else {
					this.addScore(15);
					this.report.push('food');
				}
				this.map[r][c]=0;
				this.f1[r][c]=0;
				this.food--;
				if (this.food==0) {
					this.gameStatus=1;
					return;
				}
			}
		}
		this.gmove();
		this.testCrash();
		if (this.gameStatus>0) return;
		if (this.pill) {
			this.pillCnt--;
			if (this.pillCnt==0) {
				this.pill=false;
			}
		}
		this.pac.md=0;
	},
	
	// pacman
	pMove: function() {
		with (this.pac) {
			md&=dir;
			if (md) {
				dx=(md&3);
				dy=(md&12);
				if (dx) {
					dir=3;
					dx=this.tx[dx];
				}
				if (dy) {
					dir=12;
					dy=this.ty[dy];
				}
			}
			if (this.f2[r][c]) {
				this.setPacNextNode();
			}
		}
	},
	setPacNextNode: function() {
		with (this.pac) {
			if (md) {
				var n=this.nextNodes[md][r][c];
				if (n) {
					var d=this.f2[n[0]][n[1]] & (15^this.t3[md]);
					while ((d) && (this.t2[d].length<2)) {
						var nn=this.nextNodes[d][n[0]][n[1]];
						if (nn) {
							n=nn;
							d=this.f2[n[0]][n[1]] & (15^this.t3[d]);
							if (!d) {
								this.pacNext=[r,c];
								return;
							}
						}
						else {
							break;
						}
					}
					this.pacNext=n;
					return;
				}
			}
			pacNext=null;
		}
	},
	updateMapMask: function() {
		var m=this.mapMask;
		var r=this.pac.r;
		var c=this.pac.c;
		if (r>1) {
			if (c>1) m[r-1][c-1]=true;
			if (c<20) m[r-1][c+1]=true;
			m[r-1][c]=true;
		}
		if (c>1) m[r][c-1]=true;
		if (c<20) m[r][c+1]=true;
		m[r][c]=true;
		if (r<14) {
			if (c>1) m[r+1][c-1]=true;
			if (c<20) m[r+1][c+1]=true;
			m[r+1][c]=true;
		}
	},
	
	// ghosts
	setGhost: function(i,r,c) {
		var gi=this.g[i];
		var s=gi.s;
		this.fft[gi.r][gi.c]|=gi.bid;
	},
	gmove: function() {
		for (var i=1; i<=4; i++) {
			with (this.g[i]) {
				this.fft[r][c]&=bm;
				if (s==2) this.gMove2(i)
				else if (s==1) this.gMove1(i)
				else this.gMove0(i);
				this.setGhost(i,r,c);
			}
		}
	},
	gMove0: function(i) {
		with (this.g[i]) {
			if ((this.tileFree(i,7,12)) && ((this.pac.dx+this.pac.dy)!=0)) {
				if ((z>16) || (this.getRand(8)<1)) {
					s++;
					r=7; c=12;
					d=2;
					this.report.push('gate');
				}
				else {
					z++;
				}
			}
		}
	},
	gMove1: function(i) {
		with (this.g[i]) {
			if (this.tileFree(i,7,13)) {
				c=13;
				d=this.t2[this.ghostInitDir][this.getRand(this.t2[this.ghostInitDir].length)];
				s++;
			}
		}
	},
	gMove2: function(i) {
		if ((this.pill) && (this.pillCnt%2)) return;
		with (this.g[i]) {
			var x= this.f2[r][c];
			if (x>0) {
				var gdm= x&(15^this.t3[d]);
				if (Math.random()<this.pGstrat) this.gmoveStrat(i,gdm)
				else this.gmoveRand(i,gdm);
			}
			if (d>0) {
				if (this.tileFree(i,this.trunkBy(r+this.ty[d],14),this.trunkBy(c+this.tx[d],20))) this.gSet(i)
				else this.reverseStuck(i,x);
			}
		}
	},
	gSet: function(i) {
		with (this.g[i]) {
			c=this.trunkBy(c+this.tx[d],20);
			r=this.trunkBy(r+this.ty[d],14);
		}
	},
	gmoveRand: function(i,k) {
		this.g[i].d=this.t2[k][this.getRand(this.t2[k].length)];
	},
	gmoveStrat: function(i,gdm) {
		var desx, desy, px, py;
		var gi=this.g[i];
		if (this.pill) {
			desy=gi.r-this.pac.r;
			desx=gi.c-this.pac.c;
		}
		else if ((this.pacNext) && (Math.random()<this.pGsLookahead)) {
			py=this.pacNext[0];
			px=this.pacNext[1];
			desy=py-gi.r;
			desx=px-gi.c;
			if ((desx==0) && (desy==0)) {
				desy=this.pac.r-gi.r;
				desx=this.pac.c-gi.c;
			}
		}
		else {
			desy=this.pac.r-gi.r;
			desx=this.pac.c-gi.c;
		}
		gi.d=0;
		if (desx!=0) gi.d= (desx>0)? 1:2;
		if (desy!=0) gi.d|= (desy>0)? 8:4;
		gi.d&=gdm;
		if (gi.d>0) this.gmoveRand(i,gi.d)
		else this.gmoveRand(i,gdm);
	},
	reverseStuck: function(i,x) {
		with (this.g[i]) {
			if (x==0) {
				if (this.tileFree(i,this.trunkBy(r-this.ty[d],14),this.trunkBy(c-this.tx[d],20))) d=this.t3[d];
			}
			else {
				var vd= this.t2[x][this.getRand(this.t2[x].length)];
				if (this.tileFree(i,r+this.ty[vd],c+this.tx[vd])) {
					d=vd;
					this.gSet(i);
				}
			}
		}
	},
	
	testCrash: function() {
		var crash=0;
		var pr=this.pac.r;
		var pc=this.pac.c;
		for (i=1; i<=4; i++) {
			var gi=this.g[i];
			if ((gi.s==2) && (gi.r==pr) && (gi.c==pc)) {
				crash=i;
				if (this.pill) {
					this.report.push('ghostcaught');
					this.bonusReport.push(this.ghostBonus[this.ghostCnt]);
					this.addScore(this.ghostBonus[this.ghostCnt]);
					if (this.ghostCnt<3) this.ghostCnt++;
					this.fft[gi.r][gi.c]&=gi.bm;
					this.gHome(i);
				}
			}
		}
		if (crash>0) {
			if (!this.pill) {
				this.resetPac();
				return true;
			}
		}
		return false;
	},
	
	resetPac: function() {
		this.nLife--;
		if (this.nLife==0) {
			this.gameOver();
			this.report.push('gameover');
			return;
		}
		for (i=1; i<=4; i++) {
			var gi=this.g[i];
			this.fft[gi.r][gi.c]&=gi.bm;
			this.gHome(i);
		}
		this.pHome();
		this.report.push('endlife');
	},

	bonusLife: function () {
		this.nLife++;
		this.report.push('bonuslife');
	},
	
	gameOver: function() {
		this.gameStatus=-1;
	},
	endLevel: function() {
		this.gameStatus=2;
	},
	setDir: function(s) {
		var d=0;
		switch (s) {
			case 'e': // east/right
			case 'r':
				d=1; break;
			case 'w': // west/left
			case 'l':
				d=2; break;
			case 'n': // north/up
			case 'u':
				d=4; break;
			case 's': // south/down
			case 'd':
				d=8; break;
		}
		if (d) {
			this.pacDir=this.pac.md=d;
		}
		return d&this.pac.dir;
	}
	
	// random maze

}

// eof
