diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edf4707 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.app +*.zip +.bash_history +.svn +*.dSYM +*.swp +*.sw* +.DS_Store +scripts/level*.png +*~ + +# Local work dirs +staging/ +orig/ + +# test files +orig.html + + diff --git a/cat.html b/cat.html index b052d25..d5a35e2 100644 --- a/cat.html +++ b/cat.html @@ -17,6 +17,7 @@ var things = []; var myScore; var curpath = []; +var pathdir = -1; var pathvalid = false; var MAXDIRS = 4; @@ -27,11 +28,18 @@ var GRAVITY = 0.5; var GRIDSIZE = 80; var THINGSIZE = 64; +var LINEWIDTH=2; +var CROSSWIDTH=2; + var GRIDW = 5; var GRIDH = 5; +var PARADELENGTH = 3; + function getgridthing(gridx, gridy) { var i; + if (things == undefined) return null; + for (i = 0; i < things.length; i += 1) { if ((things[i].gridx == gridx) && (things[i].gridy == gridy)) { return things[i]; @@ -55,17 +63,70 @@ function getthingxy(x, y) { function clearpath() { curpath = []; + pathdir = -1; pathvalid = false; } +function pathcomplete() { + // get path type + switch(getpathtype()) { + case "chomp": + if ((curpath.length >= 2) && + (curpath[0].type == "cat") && + (curpath[0].eaten == false)) { + var i; + var ok = true; + // everything else is food? + for (i = 1; i < curpath.length; i++) { + if (curpath[i].type != "food") { + ok = false; + break; + } + } + if (ok) { + return true; + } + } + break; + case "parade": + // long enough? + if (curpath.length >= PARADELENGTH) { + var count = 0; + var i; + // includes <= 1 llama? + for (i = 1; i < curpath.length; i++) { + if (curpath[i].type == "llama") { + count++; + } + } + if (count <= 1) { + return true; + } + } + break; + } + return false; +} + + +function backspacepath() { + // remove last thing in path + curpath.splice(curpath.length-1, 1); + + validatepath(); + + dumppath("Reduced path to: ",curpath); +} + function addtopath(what) { console.log("addpath() " + what.name); - dumppath("addpath pre: ", curpath); + //dumppath("addpath pre: ", curpath); curpath.push(what); - pathvalid = true; + validatepath(); + if (curpath.length == 1) { console.log("Starting path with " + what.name); } else { @@ -73,6 +134,18 @@ function addtopath(what) { } } +function validatepath() { + if (pathcomplete()) { + pathvalid = true; + } else { + pathvalid = false; + } + + if (curpath.length == 2) { + pathdir = getdir(curpath[0], curpath[1]); + } +} + function pathcontains(what) { var i; if (curpath == undefined) return false; @@ -87,6 +160,10 @@ function pathcontains(what) { function isvalidpath(mypath) { var valid = true; + var firstcat = false; + var fcount = false; + var lcount = 0; + var i; for (i = 0; i < mypath.length - 1; i++) { var thisone,nextone; @@ -100,18 +177,36 @@ function isvalidpath(mypath) { */ nextone = mypath[i+1]; + if (thisone.type == "food") { + fcount++; + } + if (thisone.type == "llama") { + lcount++; + } + if ((thisone.type == "cat") && nextone.type == "cat") { // lines of cats are ok + } else if ((thisone.type == "cat") && nextone.type == "llama") { + // cat -> llama is only okay if there are no other llamas or food in the path + if (lcount >= 1) { + return false; + } + if (fcount >= 1) { + return false; + } + } else if ((thisone.type == "llama") && nextone.type == "cat") { + // ok } else if ((i == 0) && (thisone.type == "cat") && (nextone.type == "food")) { // first cat -> food is ok + firstcat = true; + } else if ((i != 0) && firstcat && (thisone.type == "food")) { + // not the first one, first one was a cat, this one is food } else { // not ok valid = false; break; } } - //var pr = "isvalidpath()? " + valid + " "; - //dumppath(pr, mypath); return valid; } @@ -119,9 +214,10 @@ function isvalidpath(mypath) { function canextendpath(overthing) { if (!overthing) return false; - if (overthing && - isadjacent(overthing, curpath[curpath.length-1]) && // adjacent to last thing in path? - !pathcontains(overthing) ) { // path doesn't already contain this? + if ( isadjacent(overthing, curpath[curpath.length-1]) && // adjacent to last thing in path? + !pathcontains(overthing) && // path doesn't already contain this? + (getpathtype() == "parade" || isinpathdir(overthing)) // path in a straight line + ) { // create a fake new path containing this. var fakepath = curpath.slice(); fakepath.push(overthing); @@ -136,6 +232,7 @@ function canextendpath(overthing) { function isadjacent(thing1, thing2) { // is thing1 adjacent to thing2? var newgridx,newgridy; + var i; if (thing1 == thing2) return false; @@ -149,19 +246,57 @@ function isadjacent(thing1, thing2) { return false; } -function startGame() { - var x,y; - for (y = 0; y < GRIDH; y++) { - for (x = 0; x < GRIDW; x++) { - things.push(new thing(x, y, "random")); - } +function isinpathdir(what) { + var thisdir; + if (curpath.length <= 1) { + return true; } - myGameArea.start(); + // get dir from last cell to what + thisdir = getdir(curpath[curpath.length - 1], what); + if ((thisdir != -1) && (thisdir == pathdir)) { + return true; + } + return false; +} + +function getdir(thing1, thing2) { + var i; + for (i = 0; i < MAXDIRS; i++) { + var newgx = thing1.gridx + DIRXMOD[i]; + var newgy = thing1.gridy + DIRYMOD[i]; + + if ((thing2.gridx == newgx) && (thing2.gridy == newgy)) { + return i; + } + } + return -1; +} + +function startGame() { + game.start(); +} + +// valid types: +// none +// chomp +// parade +function getpathtype() { + if (curpath.length <= 1) return "none" + + if (curpath[0].type == "cat") { + if (curpath[1].type == "food") { + return "chomp"; + } else if ((curpath[1].type == "cat") || (curpath[1].type == "llama")) { + return "parade"; + } + } + return "none"; } function dumppath(prefix,arr) { var str; + var i; str = ""; for (i = 0; i < arr.length; i++) { str = str + " " + arr[i].name; @@ -170,6 +305,7 @@ function dumppath(prefix,arr) { } function thingsmoving() { + var i; for (i = 0; i < things.length; i += 1) { if (things[i].state != "stop") { return true; @@ -178,21 +314,97 @@ function thingsmoving() { return false; } -var myGameArea = { +function canstartpath(what) { + if (what.type != "cat") { + // only cats can start paths +console.log("not a cat"); + return false; + } + + if (isadjacenttotype(what, "llama")) { + // cats adjacent to llamas are frozen +console.log("next to a llama"); + return false; + } + + return true; +} + + +function isadjacenttotype(what, wanttype) { + var newgridx,newgridy; + var i; + + for (i = 0; i < MAXDIRS; i++) { + newgridx = what.gridx + DIRXMOD[i]; + newgridy = what.gridy + DIRYMOD[i]; + + if (isonscreen(newgridx,newgridy)) { + adjthing = getgridthing(newgridx, newgridy); + if ((adjthing != undefined) && (adjthing.type == wanttype)) { + return true; + } + } + } + return false; +} + +function drawarrowhead(ctx, x1, y1, x2, y2, col) { + var startrads,endrads; + + startrads = Math.atan((y2 - y1) / (x2 - x1)); + if (x2 >= x1) { + startrads += -90*Math.PI/180; + } else { + startrads += 90*Math.PI/180; + } + + endrads = Math.atan((y2 - y1) / (x2 - x1)); + if (x2 >= x1) { + endrads += 90 * Math.PI / 180; + } else { + endrads += -90 * Math.PI / 180; + } + + ctx.strokeStyle = col; + ctx.fillStyle = col; + + ctx.save(); + ctx.beginPath(); + ctx.translate(x2,y2); + ctx.rotate(endrads); + ctx.moveTo(0,0); + ctx.lineTo(10,15); + ctx.lineTo(-10,15); + ctx.closePath(); + ctx.restore(); + ctx.fill(); +} + +var game = { canvas : document.createElement("canvas"), start : function() { - this.canvas.width = 480; - this.canvas.height = 640; - this.context = this.canvas.getContext("2d"); - document.body.insertBefore(this.canvas, document.body.childNodes[0]); - this.frameNo = 0; - this.interval = setInterval(updateGameArea, 20); - - this.canvas.addEventListener('click', this.handleclick, false); - this.canvas.addEventListener('mousedown', this.handlemousedown, false); - this.canvas.addEventListener('mouseup', this.handlemouseup, false); - this.canvas.addEventListener('mousemove', this.handlemousemove, false); - }, + var x,y; + this.canvas.width = 480; + this.canvas.height = 640; + this.context = this.canvas.getContext("2d"); + document.body.insertBefore(this.canvas, document.body.childNodes[0]); + this.frameNo = 0; + this.interval = setInterval(updateGameArea, 20); + + this.canvas.addEventListener('click', this.handleclick, false); + this.canvas.addEventListener('mousedown', this.handlemousedown, false); + this.canvas.addEventListener('mouseup', this.handlemouseup, false); + this.canvas.addEventListener('mousemove', this.handlemousemove, false); + + // populate inital things + for (y = 0; y < GRIDH; y++) { + for (x = 0; x < GRIDW; x++) { + // start off above the grid + things.push(new thing(x, y - GRIDH, "random")); + } + } + }, clear : function() { this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); @@ -201,6 +413,7 @@ var myGameArea = { draw : function() { this.context.strokeStyle = "black"; this.context.beginPath(); + // draw grid for (y = 0; y < GRIDH*GRIDSIZE; y += GRIDSIZE) { for (x = 0; x < GRIDW*GRIDSIZE; x += GRIDSIZE) { this.context.rect(x, y, GRIDSIZE-1, GRIDSIZE-1); @@ -210,6 +423,43 @@ var myGameArea = { this.context.stroke(); }, + drawpath : function() { + var ctx = this.context; + var col; + var i; + + if ((curpath == undefined) || (curpath.length < 1)) { + return; + } + + if (pathvalid) { + col = "green"; + } else { + col = "red"; + } + ctx.strokeStyle = col; + ctx.lineWidth = LINEWIDTH; + + // draw line + ctx.beginPath(); + ctx.moveTo(curpath[0].x + THINGSIZE/2, curpath[0].y + THINGSIZE/2); + for (i = 1; i < curpath.length; i++) { + ctx.lineTo(curpath[i].x + THINGSIZE/2, curpath[i].y + THINGSIZE/2); + } + ctx.stroke(); + + if (curpath.length >= 2) { + x1 = curpath[curpath.length-2].x + (THINGSIZE/2); + y1 = curpath[curpath.length-2].y + (THINGSIZE/2); + + x2 = curpath[curpath.length-1].x + (THINGSIZE/2); + y2 = curpath[curpath.length-1].y + (THINGSIZE/2); + + drawarrowhead(ctx, x1, y1, x2, y2, col); + } + + }, + /* handleclick : function(event) { // make sure nothing is moving @@ -232,7 +482,7 @@ var myGameArea = { // did you click on an object? var onthing = getthingxy(event.pageX,event.pageY); - if (onthing) { + if (onthing && canstartpath(onthing)) { console.log("Initial click on " + onthing.name); addtopath(onthing); } @@ -245,17 +495,25 @@ console.log("Initial click on " + onthing.name); // existing path? if (curpath != undefined && curpath.length > 0) { var overthing = getthingxy(event.pageX,event.pageY); - var lastinpath; + var lastinpath,secondlast; + if (curpath == undefined) { lastinpath = null; } else { lastinpath = curpath[curpath.length-1]; + if (curpath.length >= 2) { + secondlast = curpath[curpath.length-2]; + } else { + secondlast = null; + } } - if (canextendpath(overthing)) { + if ((secondlast != undefined) && (overthing == secondlast)) { + backspacepath(); + } else if (canextendpath(overthing)) { // add it to the path addtopath(overthing); - } else if (pathcontains(overthing)) { + } else if (pathcontains(overthing) && pathcomplete()) { pathvalid = true; } else { pathvalid = false; @@ -266,12 +524,20 @@ console.log("Initial click on " + onthing.name); handlemouseup : function(event) { var ok = true; var overthing = getthingxy(event.pageX,event.pageY); - if (thingsmoving()) { + var ptype = getpathtype(); + + if ((curpath == undefined) || (curpath.length == 0)) { + console.log("mouseup() - no path"); + ok = false; + } else if (thingsmoving()) { console.log("mouseup() - things are moving"); ok = false; } else if (!isvalidpath(curpath)) { console.log("mouseup() - path isn't valid"); ok = false; + } else if (!pathcomplete(curpath)) { + console.log("mouseup() - path ("+ ptype + ") isn't complete"); + ok = false; } else if ((overthing == undefined) || !pathcontains(overthing)) { console.log("mouseup() - not over something in path"); ok = false; @@ -282,11 +548,29 @@ console.log("Initial click on " + onthing.name); return; } - // kill all in path - while (curpath != undefined && curpath.length > 0) { - curpath[0].kill(); - curpath.splice(0, 1); + // figure out path type + switch(ptype) { + case "chomp": + // killall except first and last + if (curpath.length > 2) { + var i; + for (i = 1; i < curpath.length - 1; i++) { + curpath[i].kill(); + } + } + + // first one chomps last one + curpath[0].chomp(curpath[curpath.length - 1]); + break; + case "parade": + // kill all in path + while (curpath != undefined && curpath.length > 0) { + curpath[0].kill(); + curpath.splice(0, 1); + } + break; } + clearpath(); }, } @@ -310,13 +594,22 @@ function getrandomname() { return name; } +function isonscreen(x,y) { + if ((x < 0) || (y < 0) || (x >= GRIDW) || (y >= GRIDH)) { + return false; + } + return true; +} + function getrandomtype() { var pct,type; pct = Math.random() * 100; - if ( pct < 50) { + if ( pct < 45) { type = "cat"; - } else { + } else if ( pct < 90) { type = "food"; + } else { + type = "llama"; } return type; } @@ -330,18 +623,26 @@ function thing(gridx, gridy, type) { } this.type = type; - if (this.type == "cat") { - this.color = "#b5dea8"; - } else if (type == "food") { - this.color = "#d8db03"; - } else { - this.color = getrandomcolour(); + switch (this.type) { + case "cat": + this.color = "#b5dea8"; + break; + case "food": + this.color = "#d8db03"; + break; + case "llama": + this.color = "#cccccc"; + break; + default: // should never happen + this.color = getrandomcolour(); + break; } this.name = type + "-" + getrandomname(); this.gridx = gridx; if (gridy == "top") { + var i; highest = 999; // find highest for (i = 0; i < things.length; i += 1) { @@ -356,33 +657,58 @@ function thing(gridx, gridy, type) { this.gridy = gridy; this.yspeed = 0; this.state = "stop"; - this.x = gridx * GRIDSIZE; - this.y = gridy * GRIDSIZE; + + this.x = gridx * GRIDSIZE + (GRIDSIZE/2) - (THINGSIZE/2); + this.y = gridy * GRIDSIZE + (GRIDSIZE/2) - (THINGSIZE/2); + this.eaten = false; this.draw = function() { var yoff; var inpath = false; - ctx = myGameArea.context; - ctx.fillStyle = this.color; + ctx = game.context; // draw myself + ctx.fillStyle = this.color; ctx.fillRect(this.x, this.y, this.width, this.height); + // frozen cat? + if (this.type == "cat") { + if (isadjacenttotype(this, "llama")) { + // cross out + ctx.fillStyle = "red"; + ctx.lineWidth = CROSSWIDTH; + ctx.strokeStyle = "red"; + // NW -> SE + ctx.beginPath(); + ctx.moveTo(this.x, this.y); + ctx.lineTo(this.x + THINGSIZE - 1, this.y + THINGSIZE - 1); + ctx.stroke(); + + // SW -> NE + ctx.beginPath(); + ctx.moveTo(this.x, this.y + THINGSIZE - 1); + ctx.lineTo(this.x + THINGSIZE - 1, this.y); + ctx.stroke(); + } + } + // highlight? if (pathcontains(this)) { inpath = true; } - // draw text ctx.fillStyle = "black"; ctx.fillText(this.name, this.x + 10, this.y + (THINGSIZE/2)); - ctx.fillText(this.state, this.x + 10, this.y + (THINGSIZE/2) + 10); + ctx.fillText(this.eaten ? "FULL" : "", this.x + 10, this.y + (THINGSIZE/2) + 10); + // path outline if (inpath) { ctx.beginPath(); - ctx.fillText("PATH", this.x + 10, this.y + (THINGSIZE/2) + 20); + ctx.lineWidth = 1; + ctx.fillStyle = "black"; + // outline it if (pathvalid) { ctx.strokeStyle = "green"; } else { @@ -401,8 +727,8 @@ function thing(gridx, gridy, type) { } this.snaptogrid = function() { - if (this.y % GRIDSIZE != 0) { - this.y = this.gridy * GRIDSIZE; + if (this.y % (GRIDSIZE + (GRIDSIZE/2) - (THINGSIZE/2)) != 0) { + this.y = this.gridy * GRIDSIZE + (GRIDSIZE/2) - (THINGSIZE/2); } } @@ -425,6 +751,39 @@ function thing(gridx, gridy, type) { things.splice(idx, 1); } + this.updatexy = function() { + this.x = this.gridx * GRIDSIZE + (GRIDSIZE/2) - (THINGSIZE/2); + this.y = this.gridy * GRIDSIZE + (GRIDSIZE/2) - (THINGSIZE/2); + } + + this.setgridxy = function(x,y) { + this.gridx = x; + this.gridy = y; + this.updatexy(); + } + + this.chomp = function(food) { + var origgx,origgy; + // remember cat loc + origgx = this.gridx; + origgy = this.gridy; + + // move cat to food location + this.setgridxy(food.gridx,food.gridy); + + // move food to cat location (so new object will + // be added in the correct column. + food.setgridxy(origgx, origgy); + + // kill food + food.kill(); + + // mark that we've eaten something + this.eaten = true; + + console.log("chomp"); + } + this.move = function() { // not at bottom and nothing fixed below us? var dofall = false; @@ -475,22 +834,25 @@ function thing(gridx, gridy, type) { function updateGameArea() { var x, height, gap, minHeight, maxHeight, minGap, maxGap; + var i; - myGameArea.clear(); - myGameArea.draw(); - myGameArea.frameNo += 1; + game.clear(); + game.draw(); + game.frameNo += 1; for (i = 0; i < things.length; i += 1) { - things[i].draw(); things[i].move(); + things[i].draw(); } - //myScore.text="SCORE: " + myGameArea.frameNo; + game.drawpath(); + + //myScore.text="SCORE: " + game.frameNo; //myScore.draw(); //myGamePiece.move(); //myGamePiece.update(); } function everyinterval(n) { - if ((myGameArea.frameNo / n) % 1 == 0) {return true;} + if ((game.frameNo / n) % 1 == 0) {return true;} return false; } diff --git a/todo b/todo index 94c17d0..5aef0d6 100644 --- a/todo +++ b/todo @@ -77,8 +77,64 @@ start: *click one, rest fall down and a new one is generated *drag across multi, all disappear, rest fall, new are generated *don't allow clicks until all grid locations are full - *allow cat->food or cat->...->cat -cat eating food should take its place +*cat eating food should take its place +*parade must be at least 4 +*cat can only eat food once, but can eat multiple foods in one go +*dont let chomp lines change dir (but parades can) +*allow reversing a line +*draw arrow for lines + +*llama + *cats next to it can't move + *cat parades can include one (but only one). +*draw cats in centre of grid squares, not top left + +offset entire grid downwards to allow space for a header + +add pictures of: + cat + fat cat + scared cat + llama + food + + +make path part of game object +make things part of game object + +ideas: + llamas don't appear after the start, you just have to get rid of them all? + +ensure that start of game has at least one valid move! + +show score up the top + Score: (+20) 100 + points for chomp + points for parade + points for each empty cat + points for each full cat + extra points for llama + length 3 = 1x + length 4 = 2x + length 5 = 3x + multiplier for length + +animated points gaining + text fading out + + +animate parades +animate chomps + +end if no valid moves + after each move: + + are all cats locked? if so, game over. + + for each cell, + for each dir, + check paths for all lengths + if none found, game over