| 
									
										
										
										
											2017-04-13 14:22:01 +03:00
										 |  |  | /********************************************************************** | 
					
						
							|  |  |  | *  | 
					
						
							|  |  |  | * Simple Snake  | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * This code is designed to illustrate the non-intuitive approach to an | 
					
						
							|  |  |  | * implementation, building a snake game as a cellular automaton rather | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | * than the more obvious, set of entities (OOP) or a number of sets  | 
					
						
							| 
									
										
										
										
											2017-04-13 14:22:01 +03:00
										 |  |  | * of procedures and data structures, directly emulating the "tactile"  | 
					
						
							|  |  |  | * perception of the game, i.e. independent field, snakes, walls, apples | 
					
						
							|  |  |  | * and their interactions. | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * In this approach there are no entities, no snakes, no apples, no  | 
					
						
							|  |  |  | * walls, just a set of cells in a field and cell behaviours per game  | 
					
						
							|  |  |  | * step: | 
					
						
							|  |  |  | * 	- empty cells, apples and walls just sit there | 
					
						
							|  |  |  | * 	- "snake" cells: | 
					
						
							|  |  |  | * 		- decrement age | 
					
						
							|  |  |  | * 		- if age is 0 clear cell | 
					
						
							|  |  |  | * 		- if cell has direction (i.e. snake head) | 
					
						
							|  |  |  | * 			- if target cell is red (apple) increment age | 
					
						
							|  |  |  | * 			- color new cell in direction: | 
					
						
							|  |  |  | * 				- set age on to current age + 1 | 
					
						
							|  |  |  | * 				- set direction to current | 
					
						
							|  |  |  | * 			- clear direction | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * NOTE: that in the above description some details are omitted for  | 
					
						
							|  |  |  | * 		clarity... | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | * This code is structured in a scalable and introspective way: | 
					
						
							|  |  |  | * 	- Snake object is reusable as a prototype enabling multiple games | 
					
						
							|  |  |  | * 		to run at the same time | 
					
						
							|  |  |  | * 	- Snake implements an open external control scheme, i.e. it does not  | 
					
						
							|  |  |  | * 		impose a specific way to implementing the way to control the game | 
					
						
							|  |  |  | * 	- Simple (but not trivial) code and code structure | 
					
						
							|  |  |  | * 	- Introspective: no hidden/masked state or functionality | 
					
						
							|  |  |  | * 	- No external dependencies | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * Goals: | 
					
						
							|  |  |  | * 	- Show that the "intuitive" is not the only approach | 
					
						
							|  |  |  | * 	- Show that the "intuitive" is not allways the simplest | 
					
						
							|  |  |  | * 	- Show one approach to a scalable yet simple architecture | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * | 
					
						
							| 
									
										
										
										
											2017-04-13 14:22:01 +03:00
										 |  |  | **********************************************************************/ | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-06 02:32:35 +03:00
										 |  |  | function makeEvent(handler_attr){ | 
					
						
							|  |  |  | 	return function(func){ | 
					
						
							| 
									
										
										
										
											2017-04-12 17:12:02 +03:00
										 |  |  | 		if(func === null){ | 
					
						
							|  |  |  | 			delete this[handler_attr] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		} else if(func instanceof Function){ | 
					
						
							| 
									
										
										
										
											2017-04-06 02:32:35 +03:00
										 |  |  | 			var handlers = this[handler_attr] = this[handler_attr] || [] | 
					
						
							|  |  |  | 			handlers.push(func) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			var args = [].slice.call(arguments) | 
					
						
							|  |  |  | 			this[handler_attr] | 
					
						
							|  |  |  | 				&& this[handler_attr] | 
					
						
							|  |  |  | 					.forEach(function(handler){ handler.apply(that, args) }) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return this | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | var Snake = { | 
					
						
							|  |  |  | 	config: { | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 		field_size: 32, | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | 		interval: 150, | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 	}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_field: null, | 
					
						
							|  |  |  | 	_cells: null, | 
					
						
							| 
									
										
										
										
											2017-04-05 05:19:55 +03:00
										 |  |  | 	players: null, | 
					
						
							|  |  |  | 	field_size: null, | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 	get random_point(){ | 
					
						
							|  |  |  | 		var cells = this._cells | 
					
						
							|  |  |  | 		var l = cells.length | 
					
						
							|  |  |  | 		var w = this.field_size.width | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		do { | 
					
						
							|  |  |  | 			var i = Math.floor(Math.random() * l) | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 		} while(cells[i].classList.length > 0) | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		return { | 
					
						
							|  |  |  | 			x: i%w, | 
					
						
							|  |  |  | 			y: Math.floor(i/w), | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 	get random_direction(){ | 
					
						
							|  |  |  | 		return ('nesw')[Math.floor(Math.random() * 4)] }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 	// utils...
 | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 	call: function(func){ | 
					
						
							|  |  |  | 		return func.apply(this, [].slice.call(arguments, 1)) }, | 
					
						
							|  |  |  | 	apply: function(func, args){  | 
					
						
							|  |  |  | 		return func.apply(this, args) }, | 
					
						
							| 
									
										
										
										
											2017-04-12 02:12:32 +03:00
										 |  |  | 	normalize_point: function(point){ | 
					
						
							|  |  |  | 		point = point || {} | 
					
						
							|  |  |  | 		var w = this.field_size.width | 
					
						
							|  |  |  | 		var x = point.x % w | 
					
						
							|  |  |  | 		x = x < 0 ? (x + w) : x | 
					
						
							|  |  |  | 		var h = this.field_size.height | 
					
						
							|  |  |  | 		var y = point.y % h | 
					
						
							|  |  |  | 		y = y < 0 ? (y + h) : y | 
					
						
							|  |  |  | 		return { x: x, y: y } | 
					
						
							|  |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 	_make_field: function(w){ | 
					
						
							|  |  |  | 		var l = [] | 
					
						
							|  |  |  | 		l.length = w || this.config.field_size | 
					
						
							|  |  |  | 		l.fill('<td/>') | 
					
						
							|  |  |  | 		this._field.innerHTML =  | 
					
						
							|  |  |  | 			`<table class="field" cellspacing="0">\n${  | 
					
						
							|  |  |  | 				l.map(function(){  | 
					
						
							|  |  |  | 					return `  <tr> ${ l.join('') } </tr>`  | 
					
						
							|  |  |  | 				}).join('\n')  | 
					
						
							|  |  |  | 			}\n</table>` | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | 	_tick: function(){ | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 		var that = this | 
					
						
							|  |  |  | 		var l = this._cells.length | 
					
						
							|  |  |  | 		var w = this.field_size.width | 
					
						
							|  |  |  | 		var h = this.field_size.height | 
					
						
							|  |  |  | 		var tick = this.__tick = (this.__tick + 1 || 0) | 
					
						
							|  |  |  | 		var directions = 'neswn' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		this._cells.forEach(function(cell, i){ | 
					
						
							|  |  |  | 			var color = cell.style.backgroundColor | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | 			// skip cells we touched on this tick...
 | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 			if(cell.tick == tick){ | 
					
						
							|  |  |  | 				return | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// snake...
 | 
					
						
							|  |  |  | 			if(cell.age != null){ | 
					
						
							|  |  |  | 				// handle cell age...
 | 
					
						
							|  |  |  | 				if(cell.age == 0){ | 
					
						
							|  |  |  | 					delete cell.age | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 					cell.classList.remove('snake') | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 					cell.style.backgroundColor = '' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					cell.age -= 1 | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | 				// snake head -> move...
 | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 				var direction = cell.direction | 
					
						
							| 
									
										
										
										
											2017-04-10 19:49:11 +03:00
										 |  |  | 				if(directions.indexOf(direction) >= 0){ | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 					// turn...
 | 
					
						
							| 
									
										
										
										
											2017-04-10 19:49:11 +03:00
										 |  |  | 					if(that.players[color] != ''){ | 
					
						
							|  |  |  | 						var turn = that.players[color] || '' | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 						var j = turn == 'left' ? directions.indexOf(direction) - 1 | 
					
						
							|  |  |  | 							: directions.indexOf(direction) + 1 | 
					
						
							|  |  |  | 						j = j < 0 ? 3 : j | 
					
						
							|  |  |  | 						direction = directions[j] | 
					
						
							|  |  |  | 						that.players[color] = '' | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | 					// get next cell index...
 | 
					
						
							| 
									
										
										
										
											2017-04-10 19:49:11 +03:00
										 |  |  | 					var next =  | 
					
						
							|  |  |  | 						direction == 'n' ?  | 
					
						
							|  |  |  | 							(i < w ? l - w + i : i - w) | 
					
						
							|  |  |  | 						: direction == 's' ?  | 
					
						
							|  |  |  | 							(i > (l-w-1) ? i - (l-w) : i + w) | 
					
						
							|  |  |  | 						: direction == 'e' ?  | 
					
						
							|  |  |  | 							((i+1)%w == 0 ? i - (w-1) : i + 1) | 
					
						
							|  |  |  | 						: (i%w == 0 ? i + (w-1) : i - 1) | 
					
						
							|  |  |  | 					next = that._cells[next] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 					var age = cell.age | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 					var move = false | 
					
						
							| 
									
										
										
										
											2017-04-07 13:58:44 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-12 02:12:32 +03:00
										 |  |  | 					// special case: other snake's head -> kill both...
 | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 					if(next.direction){ | 
					
						
							| 
									
										
										
										
											2017-04-06 02:32:35 +03:00
										 |  |  | 						var other = next.style.backgroundColor | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 						next.classList.remove('snake') | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 						next.style.backgroundColor = '' | 
					
						
							|  |  |  | 						// NOTE: we are not deleteing .direction here as 
 | 
					
						
							|  |  |  | 						//		we can have upto 4 snakes colliding...
 | 
					
						
							| 
									
										
										
										
											2017-04-04 02:11:41 +03:00
										 |  |  | 						next.direction = '' | 
					
						
							| 
									
										
										
										
											2017-04-13 17:44:01 +03:00
										 |  |  | 						that.snakeKilled(other, next.age+1) | 
					
						
							|  |  |  | 						that.snakeKilled(color, age+2) | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 						delete next.age | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | 					// apple -> increment age...
 | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 					} else if(next.classList.contains('apple')){ | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 						age += 1 | 
					
						
							|  |  |  | 						move = true | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 						next.classList.remove('apple') | 
					
						
							| 
									
										
										
										
											2017-04-13 17:44:01 +03:00
										 |  |  | 						that.appleEaten(color, age+2) | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-12 02:12:32 +03:00
										 |  |  | 					// empty -> just move...
 | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 					} else if(next.classList.length == 0){ | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 						move = true | 
					
						
							| 
									
										
										
										
											2017-04-06 02:32:35 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-12 02:12:32 +03:00
										 |  |  | 					// other -> kill...
 | 
					
						
							| 
									
										
										
										
											2017-04-06 02:32:35 +03:00
										 |  |  | 					} else { | 
					
						
							| 
									
										
										
										
											2017-04-13 17:44:01 +03:00
										 |  |  | 						that.snakeKilled(color, age+2) | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-06 02:32:35 +03:00
										 |  |  | 					// do the move...
 | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 					if(move){ | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 						next.tick = tick | 
					
						
							|  |  |  | 						next.style.backgroundColor = color | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 						next.classList.add('snake') | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 						next.age = age + 1 | 
					
						
							|  |  |  | 						next.direction = direction | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					delete cell.direction | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			cell.tick = tick | 
					
						
							|  |  |  | 		}) | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 		this.tick(tick) | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 	}, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 	// constructors...
 | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | 	snake: function(color, age, point, direction){ | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 		point = this.normalize_point(point || this.random_point) | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 		var head = this._cells[point.x + point.y * this.field_size.width] | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 		head.style.backgroundColor = color | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 		head.classList.add('snake') | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 		head.direction = direction || this.random_direction | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 		head.age = (age || 5) - 1 | 
					
						
							|  |  |  | 		this.players[color] = '' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return this | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 			.snakeBorn(color) | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 	apple: function(point){ | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 		point = this.normalize_point(point || this.random_point) | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 		var c = this._cells[point.x + point.y * this.field_size.width] | 
					
						
							|  |  |  | 		c.classList.add('apple') | 
					
						
							|  |  |  | 		c.style.backgroundColor = '' | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 		return this | 
					
						
							|  |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 	wall: function(point, direction, length){ | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 		direction = direction || this.random_direction | 
					
						
							|  |  |  | 		point = this.normalize_point(point || this.random_point) | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 		var x = point.x | 
					
						
							|  |  |  | 		var y = point.y | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 		length = length || 1 | 
					
						
							| 
									
										
										
										
											2017-04-08 02:32:08 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 		while(length > 0){ | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 			var c = this._cells[x + y * this.field_size.width] | 
					
						
							|  |  |  | 			c.classList.add('wall') | 
					
						
							|  |  |  | 			c.style.backgroundColor = '' | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			x += direction == 'e' ? 1 | 
					
						
							|  |  |  | 				: direction == 'w' ? -1 | 
					
						
							|  |  |  | 				: 0 | 
					
						
							|  |  |  | 			x = x < 0 ? this.field_size.width + x | 
					
						
							|  |  |  | 				: x % this.field_size.width | 
					
						
							|  |  |  | 			y += direction == 'n' ? -1 | 
					
						
							|  |  |  | 				: direction == 's' ? 1 | 
					
						
							|  |  |  | 				: 0 | 
					
						
							|  |  |  | 			y = y < 0 ? this.field_size.height + y | 
					
						
							|  |  |  | 				: y % this.field_size.height | 
					
						
							|  |  |  | 			length -= 1 | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2017-04-08 02:32:08 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-05 05:13:22 +03:00
										 |  |  | 		return this | 
					
						
							|  |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 	level: function(level){ | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 		level.forEach(function(wall){ | 
					
						
							|  |  |  | 			that.wall.apply(that, wall) }) | 
					
						
							|  |  |  | 		return this | 
					
						
							|  |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-06 02:10:41 +03:00
										 |  |  | 	// events...
 | 
					
						
							| 
									
										
										
										
											2017-04-06 02:32:35 +03:00
										 |  |  | 	snakeKilled: makeEvent('__killHandlers'), | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 	snakeBorn: makeEvent('__birthHandlers'), | 
					
						
							|  |  |  | 	appleEaten: makeEvent('__appleEatenHandlers'), | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 	tick: makeEvent('__tickHandlers'), | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 	gameStarted: makeEvent('__startHandlers'), | 
					
						
							|  |  |  | 	gameStopped: makeEvent('__stopHandlers'), | 
					
						
							| 
									
										
										
										
											2017-04-06 02:10:41 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 	// actions...
 | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 	setup: function(field, size, interval){ | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 		this.config.field_size = size || this.config.field_size | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 		this.config.interval = interval || this.config.interval | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 		field = field || this._field | 
					
						
							|  |  |  | 		field = this._field = typeof(field) == typeof('str') ? document.querySelector(field) | 
					
						
							|  |  |  | 			: field | 
					
						
							|  |  |  | 		this._make_field() | 
					
						
							|  |  |  | 		this._cells = [].slice.call(field.querySelectorAll('td')) | 
					
						
							|  |  |  | 		this.field_size = { | 
					
						
							|  |  |  | 			width: field.querySelector('tr').querySelectorAll('td').length, | 
					
						
							|  |  |  | 			height: field.querySelectorAll('tr').length, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		this.players = {} | 
					
						
							|  |  |  | 		return this | 
					
						
							| 
									
										
										
										
											2017-04-12 17:12:02 +03:00
										 |  |  | 			.appleEaten(null) | 
					
						
							|  |  |  | 			.snakeKilled(null) | 
					
						
							| 
									
										
										
										
											2017-04-12 16:50:13 +03:00
										 |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 	start: function(t){ | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 		this.__timer = this.__timer  | 
					
						
							| 
									
										
										
										
											2017-04-13 14:38:34 +03:00
										 |  |  | 			|| setInterval(this._tick.bind(this), t || this.config.interval || 200) | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 		// reset player control actions...
 | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 		Object.keys(this.players) | 
					
						
							|  |  |  | 			.forEach(function(k){ that.players[k] = '' }) | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 		return this | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 			.tick() | 
					
						
							|  |  |  | 			.gameStarted() | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 	}, | 
					
						
							|  |  |  | 	stop: function(){ | 
					
						
							|  |  |  | 		clearInterval(this.__timer) | 
					
						
							|  |  |  | 		delete this.__timer | 
					
						
							|  |  |  | 		delete this.__tick | 
					
						
							|  |  |  | 		return this | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 			.gameStopped() | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-07 13:58:44 +03:00
										 |  |  | 	pause: function(){ | 
					
						
							|  |  |  | 		return this.__timer ? this.stop() : this.start() }, | 
					
						
							| 
									
										
										
										
											2017-04-04 01:58:52 +03:00
										 |  |  | 	left: function(color){  | 
					
						
							|  |  |  | 		this.players[color || Object.keys(this.players)[0]] = 'left'  | 
					
						
							|  |  |  | 		return this | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 	right: function(color){ | 
					
						
							|  |  |  | 		this.players[color || Object.keys(this.players)[0]] = 'right'  | 
					
						
							|  |  |  | 		return this | 
					
						
							|  |  |  | 	}, | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-05 18:53:07 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-13 14:22:01 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | /*********************************************************************/ | 
					
						
							| 
									
										
										
										
											2017-04-07 01:26:11 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | var __CACHE_UPDATE_CHECK = 5*60*1000 | 
					
						
							| 
									
										
										
										
											2017-04-14 14:15:40 +03:00
										 |  |  | var __HANDLER_SET = false | 
					
						
							|  |  |  | var __DEBOUNCE_TIMEOUT = 100 | 
					
						
							|  |  |  | var __DEBOUNCE = false | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-06 02:10:41 +03:00
										 |  |  | var KEY_CONFIG = { | 
					
						
							| 
									
										
										
										
											2017-04-07 13:58:44 +03:00
										 |  |  | 	' ': ['pause'], | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 	n: setup, | 
					
						
							| 
									
										
										
										
											2017-04-08 02:32:08 +03:00
										 |  |  | 	ArrowLeft: ['left'], | 
					
						
							|  |  |  | 	ArrowRight: ['right'],  | 
					
						
							|  |  |  | 	// IE compatibility...
 | 
					
						
							|  |  |  | 	Left: ['left'], | 
					
						
							|  |  |  | 	Right: ['right'], | 
					
						
							| 
									
										
										
										
											2017-04-06 02:10:41 +03:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | function makeKeyboardHandler(snake){ | 
					
						
							| 
									
										
										
										
											2017-04-12 19:42:34 +03:00
										 |  |  | 	return function(event){ | 
					
						
							| 
									
										
										
										
											2017-04-12 23:24:22 +03:00
										 |  |  | 		clearHints() | 
					
						
							|  |  |  | 		var action = KEY_CONFIG[event.key] | 
					
						
							|  |  |  | 		action  | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 			&& (action instanceof Function ? | 
					
						
							|  |  |  | 					action.call(snake) | 
					
						
							|  |  |  | 				: action[0] in snake ? | 
					
						
							|  |  |  | 					snake[action[0]].apply(snake, action.slice(1)) | 
					
						
							|  |  |  | 				: null) }} | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | function makeTapHandler(snake){ | 
					
						
							| 
									
										
										
										
											2017-04-12 19:42:34 +03:00
										 |  |  | 	return function(event){ | 
					
						
							| 
									
										
										
										
											2017-04-14 14:15:40 +03:00
										 |  |  | 		// prevent clicks and touches from triggering the same action 
 | 
					
						
							|  |  |  | 		// twice -- only handle the first one within timeout...
 | 
					
						
							|  |  |  | 		// NOTE: this should not affect events of the same type...
 | 
					
						
							|  |  |  | 		if(__DEBOUNCE && event.type != __DEBOUNCE){ return } | 
					
						
							|  |  |  | 		__DEBOUNCE = event.type | 
					
						
							|  |  |  | 		setTimeout(function(){ __DEBOUNCE = false }, __DEBOUNCE_TIMEOUT) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-12 23:24:22 +03:00
										 |  |  | 		clearHints() | 
					
						
							|  |  |  | 		// top of screen (1/8)...
 | 
					
						
							|  |  |  | 		;(event.clientY || event.changedTouches[0].pageY) <= (document.body.clientHeight / 8) ?  | 
					
						
							|  |  |  | 			setup() | 
					
						
							|  |  |  | 		// bottom of screen 1/8...
 | 
					
						
							|  |  |  | 		: (event.clientY || event.changedTouches[0].pageY) >= (document.body.clientHeight / 8)*8 ?  | 
					
						
							|  |  |  | 			Snake.pause() | 
					
						
							|  |  |  | 		// left/right of screen...
 | 
					
						
							|  |  |  | 		: (event.clientX || event.changedTouches[0].pageX) <= (document.body.clientWidth / 2) ?  | 
					
						
							|  |  |  | 			Snake.left()  | 
					
						
							|  |  |  | 			: Snake.right() }} | 
					
						
							|  |  |  | function clearHints(){ | 
					
						
							|  |  |  | 	document.body.classList.contains('hints') | 
					
						
							|  |  |  | 		&& document.body.classList.remove('hints') } | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | function digitizeBackground(snake, walls){ | 
					
						
							|  |  |  | 	snake._cells.forEach(function(c){ | 
					
						
							|  |  |  | 		var v = Math.floor(Math.random() * 6) | 
					
						
							|  |  |  | 		// bg cell...
 | 
					
						
							|  |  |  | 		c.classList.length == 0 ? | 
					
						
							|  |  |  | 			(c.style.backgroundColor =  | 
					
						
							|  |  |  | 				`rgb(${255 - v}, ${255 - v}, ${255 - v})`) | 
					
						
							|  |  |  | 		// wall...
 | 
					
						
							|  |  |  | 		: walls && c.classList.contains('wall') ? | 
					
						
							|  |  |  | 			(c.style.backgroundColor =  | 
					
						
							|  |  |  | 				`rgb(${220 - v*2}, ${220 - v*2}, ${220 - v*2})`) | 
					
						
							|  |  |  | 		// skip the rest...
 | 
					
						
							|  |  |  | 		: null }) | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 	return snake | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-04-06 02:10:41 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-12 15:31:12 +03:00
										 |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | function setup(snake, timer, size){ | 
					
						
							| 
									
										
										
										
											2017-04-12 19:42:34 +03:00
										 |  |  | 	snake = snake || Snake | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 	// levels...
 | 
					
						
							|  |  |  | 	var A = Math.round((size || snake.config.field_size)/8) | 
					
						
							|  |  |  | 	var RANDOM3_LEVEL = [ | 
					
						
							|  |  |  | 		[null, null, A*6], | 
					
						
							|  |  |  | 		[null, null, A*6], | 
					
						
							|  |  |  | 		[null, null, A*6], | 
					
						
							|  |  |  | 	] | 
					
						
							|  |  |  | 	var HALVES_LEVEL = [ | 
					
						
							|  |  |  | 		[null, null, A*8], | 
					
						
							|  |  |  | 	] | 
					
						
							|  |  |  | 	var QUARTERS_LEVEL = [ | 
					
						
							|  |  |  | 		[null, 's', A*8], | 
					
						
							|  |  |  | 		[null, 'e', A*8], | 
					
						
							|  |  |  | 	] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 	function showScore(color, age){ | 
					
						
							|  |  |  | 		score = snake.__top_score =  | 
					
						
							|  |  |  | 			(!snake.__top_score || snake.__top_score.score < age) ? | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					color: color || '', | 
					
						
							|  |  |  | 					score: age || 0, | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				: snake.__top_score | 
					
						
							|  |  |  | 		snake._field.setAttribute('score', score.score) | 
					
						
							|  |  |  | 		snake._field.setAttribute('snake', score.color) | 
					
						
							|  |  |  | 		snake._field.setAttribute('state', ( | 
					
						
							|  |  |  | 			score.score == age && score.color == color) ? '(current)' : '') | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-14 03:37:10 +03:00
										 |  |  | 	// setup event handlers (only once)...
 | 
					
						
							| 
									
										
										
										
											2017-04-14 14:15:40 +03:00
										 |  |  | 	if(!__HANDLER_SET){ | 
					
						
							| 
									
										
										
										
											2017-04-15 15:01:33 +03:00
										 |  |  | 		// control handlers...
 | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | 		document.addEventListener('keydown', makeKeyboardHandler(snake)) | 
					
						
							| 
									
										
										
										
											2017-04-14 14:15:40 +03:00
										 |  |  | 		document.addEventListener('touchstart', makeTapHandler(snake)) | 
					
						
							| 
									
										
										
										
											2017-04-20 23:30:26 +03:00
										 |  |  | 		//document.addEventListener('mousedown', makeTapHandler(snake))
 | 
					
						
							| 
									
										
										
										
											2017-04-13 17:48:51 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-15 15:01:33 +03:00
										 |  |  | 		// cache updater...
 | 
					
						
							|  |  |  | 		var appCache = window.applicationCache | 
					
						
							|  |  |  | 		if(appCache | 
					
						
							|  |  |  | 				&& appCache.status != appCache.UNCACHED){ | 
					
						
							|  |  |  | 			appCache.addEventListener('updateready', function(){ | 
					
						
							|  |  |  | 				if(appCache.status == appCache.UPDATEREADY){ | 
					
						
							|  |  |  | 					console.log('CACHE: new version available...') | 
					
						
							|  |  |  | 					appCache.swapCache() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					confirm('New version ready, reload?') | 
					
						
							|  |  |  | 						&& location.reload() | 
					
						
							| 
									
										
										
										
											2017-04-13 17:53:15 +03:00
										 |  |  | 				} | 
					
						
							| 
									
										
										
										
											2017-04-13 17:56:39 +03:00
										 |  |  | 			}) | 
					
						
							| 
									
										
										
										
											2017-04-15 15:01:33 +03:00
										 |  |  | 			setInterval(function(){ appCache.update() }, __CACHE_UPDATE_CHECK) | 
					
						
							| 
									
										
										
										
											2017-04-13 17:48:51 +03:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2017-04-15 15:01:33 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		__HANDLER_SET = true | 
					
						
							| 
									
										
										
										
											2017-04-12 19:42:34 +03:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-04-12 15:31:12 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-15 16:53:13 +03:00
										 |  |  | 	// setup the game...
 | 
					
						
							| 
									
										
										
										
											2017-04-12 19:42:34 +03:00
										 |  |  | 	return snake | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 		// prepare the field/game...
 | 
					
						
							|  |  |  | 		.setup('.simplesnake', size, timer) | 
					
						
							|  |  |  | 		.call(digitizeBackground, snake) | 
					
						
							|  |  |  | 		.call(function(){ | 
					
						
							|  |  |  | 			this.__snake_apples = [] | 
					
						
							|  |  |  | 			return this | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// load level...
 | 
					
						
							|  |  |  | 		.level(RANDOM3_LEVEL) | 
					
						
							|  |  |  | 		//.level(HALVES_LEVEL)
 | 
					
						
							|  |  |  | 		//.level(QUARTERS_LEVEL)
 | 
					
						
							| 
									
										
										
										
											2017-04-06 01:21:32 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 		// game events / meta game rules...
 | 
					
						
							|  |  |  | 		// reconstruct eaten apples...
 | 
					
						
							| 
									
										
										
										
											2017-04-13 17:44:01 +03:00
										 |  |  | 		.appleEaten(function(color, age){  | 
					
						
							|  |  |  | 			this.apple()  | 
					
						
							| 
									
										
										
										
											2017-04-15 16:53:13 +03:00
										 |  |  | 			showScore(color, age) | 
					
						
							| 
									
										
										
										
											2017-04-13 17:44:01 +03:00
										 |  |  | 		}) | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 		// one apple per snake...
 | 
					
						
							|  |  |  | 		.snakeBorn(function(color){ | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 			this.__snake_apples.indexOf(color) < 0 | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 				&& this.apple()	 | 
					
						
							| 
									
										
										
										
											2017-04-21 20:31:16 +03:00
										 |  |  | 				&& this.__snake_apples.push(color) }) | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 		// reconstruct snakes and pause game...
 | 
					
						
							|  |  |  | 		// XXX for multiplayer reconstruct the snake on timeout and do 
 | 
					
						
							|  |  |  | 		// 		not pause...
 | 
					
						
							| 
									
										
										
										
											2017-04-13 17:44:01 +03:00
										 |  |  | 		.snakeKilled(function(color, age){  | 
					
						
							| 
									
										
										
										
											2017-04-10 19:49:11 +03:00
										 |  |  | 			this | 
					
						
							|  |  |  | 				.pause() | 
					
						
							| 
									
										
										
										
											2017-04-15 16:53:13 +03:00
										 |  |  | 				.snake(color, 3)  | 
					
						
							|  |  |  | 			showScore(color, 3) | 
					
						
							|  |  |  | 		}) | 
					
						
							| 
									
										
										
										
											2017-04-21 19:43:40 +03:00
										 |  |  | 		// indicate game state...
 | 
					
						
							|  |  |  | 		.gameStarted(function(){  | 
					
						
							|  |  |  | 			this._field.classList.remove('paused') }) | 
					
						
							|  |  |  | 		.gameStopped(function(){  | 
					
						
							|  |  |  | 			this._field.classList.add('paused') }) | 
					
						
							| 
									
										
										
										
											2017-04-16 00:19:51 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// game eleemnts...
 | 
					
						
							|  |  |  | 		.apple() | 
					
						
							| 
									
										
										
										
											2017-04-12 19:49:30 +03:00
										 |  |  | 		.snake('blue', 3) | 
					
						
							| 
									
										
										
										
											2017-04-06 02:10:41 +03:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-07 01:26:11 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-13 14:22:01 +03:00
										 |  |  | /********************************************************************** | 
					
						
							|  |  |  | * vim:set ts=4 sw=4 :                                                */ |