mirror of
				https://github.com/flynx/Course-JavaScript.git
				synced 2025-10-29 02:50:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			424 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			424 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| var DEBUG = true
 | |
| 
 | |
| function log(text){
 | |
| 	document.getElementById('log').innerHTML += text + '<br>'
 | |
| }
 | |
| 
 | |
| var Field = function(field_id){
 | |
| 	return ({
 | |
| 		// XXX HACK: get this from CSS...
 | |
| 		APPLE_COLOR: 'red',
 | |
| 		WALL_COLOR: 'gray',
 | |
| 		FIELD_COLOR: 'white',
 | |
| 		TICK: 200,
 | |
| 
 | |
| 		// interface...
 | |
| 		init: function(field_id, on_kill, on_apple_eaten){
 | |
| 			this.field = document.getElementById(field_id)
 | |
| 
 | |
| 			// this depends on topology...
 | |
| 			// NOTE: we consider that the field may not change during operation...
 | |
| 			this.cells = this.field.getElementsByTagName("td")
 | |
| 			this.height = this.field.getElementsByTagName("tr").length
 | |
| 
 | |
| 			this.cell_count = this.cells.length
 | |
| 			this.width = this.cell_count / this.height
 | |
| 
 | |
| 			this.on_kill = on_kill
 | |
| 			this.on_apple_eaten = on_apple_eaten
 | |
| 
 | |
| 			this._timer = null
 | |
| 
 | |
| 			this.reset()
 | |
| 
 | |
| 			// rotation tables...
 | |
| 			this._cw = {
 | |
| 				'n': 'e', 
 | |
| 				's': 'w',
 | |
| 				'e': 's',
 | |
| 				'w': 'n'
 | |
| 			}
 | |
| 			this._ccw = {
 | |
| 				'n': 'w', 
 | |
| 				's': 'e',
 | |
| 				'e': 'n',
 | |
| 				'w': 's'
 | |
| 			}
 | |
| 
 | |
| 			return this
 | |
| 		},
 | |
| 		// setup/reset the field to it's original state.
 | |
| 		reset: function(){
 | |
| 			this._snakes = {}
 | |
| 			this._tick = 0
 | |
| 			this.stop()
 | |
| 			for(var i=0; i < this.cells.length; i++){
 | |
| 				var cell = this.Cell(i)
 | |
| 				cell.o.style.backgroundColor = this.FIELD_COLOR
 | |
| 			}
 | |
| 		},
 | |
| 
 | |
| 		// do a single step...
 | |
| 		step: function(){
 | |
| 			var cells = this.cells
 | |
| 
 | |
| 			for(var i=0; i < cells.length; i++){
 | |
| 				var cell = this.Cell(i)
 | |
| 				// identify the object...
 | |
| 				if(this.is_snake(cell)){
 | |
| 					this.Snake(cell.o.style.backgroundColor, cell).step()
 | |
| 				}
 | |
| 			}
 | |
| 			this._tick += 1
 | |
| 		},
 | |
| 		start: function(tick){
 | |
| 			var that = this
 | |
| 			if(tick === null){
 | |
| 				tick = this.TICK
 | |
| 			}
 | |
| 			if(this._timer === null){
 | |
| 				this._timer = setInterval(function(){that.step()}, tick)
 | |
| 			}
 | |
| 		},
 | |
| 		stop: function(){
 | |
| 			if(this._timer === null){
 | |
| 				return
 | |
| 			}
 | |
| 			clearInterval(this._timer)
 | |
| 			this._timer = null
 | |
| 		},
 | |
| 
 | |
| 		// get a cell helper...
 | |
| 		Cell: function(n){
 | |
| 			var that = this
 | |
| 			var cells = this.cells
 | |
| 			return ({
 | |
| 				// NOTE: this will be null if a cell does not exist.
 | |
| 				o: cells[n],
 | |
| 				index: n,
 | |
| 				// NOTE: these are cyclic...
 | |
| 				n: function(){
 | |
| 					var t = n - that.width
 | |
| 					if(t < 0)
 | |
| 						t = that.cells.length + t
 | |
| 					return that.Cell(t)
 | |
| 				},
 | |
| 				s: function(){
 | |
| 					var t = n + that.width
 | |
| 					if(t > that.cells.length-1)
 | |
| 						t = t - that.cells.length
 | |
| 					return that.Cell(t)
 | |
| 				},
 | |
| 				e: function(){
 | |
| 					var t = n + 1
 | |
| 					if(Math.floor(t/that.width) > Math.floor(n/that.width))
 | |
| 						t = t - that.width
 | |
| 					return that.Cell(t)
 | |
| 				},
 | |
| 				w: function(){
 | |
| 					var t = n - 1
 | |
| 					if(Math.floor(t/that.width) < Math.floor(n/that.width))
 | |
| 						t = t + that.width
 | |
| 					return that.Cell(t)
 | |
| 				}
 | |
| 			})
 | |
| 		},
 | |
| 		// get a cell by it's coordinates...
 | |
| 		cell: function(x, y){
 | |
| 			return this.Cell(x + (y-1) * this.width)
 | |
| 		},
 | |
| 
 | |
| 		// add a snake to the field...
 | |
| 		// XXX BUG: size of 1 makes the snake endless...
 | |
| 		Snake: function(color, cell, direction, size){
 | |
| 			var that = this
 | |
| 
 | |
| 			// draw the snake if it does not exist...
 | |
| 			if(this._snakes[color] == null){
 | |
| 				cell.o.style.backgroundColor = color
 | |
| 				cell.o.age = size
 | |
| 				this._snakes[color] = {
 | |
| 					'direction':direction,
 | |
| 					'size': size
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// NOTE: the only things this uses from the above scope is color and that.
 | |
| 			// NOTE: color is the onlu thing that can't change in a snake.
 | |
| 			return ({
 | |
| 				// XXX BUG: the last cell of a dead snake lives an extra tick...
 | |
| 				kill: function(){
 | |
| 					// this will disable moving and advancing the snake...
 | |
| 					that._snakes[color].size = 0
 | |
| 					if(that.on_kill != null){
 | |
| 						that.on_kill(that)
 | |
| 					}
 | |
| 				},
 | |
| 				step: function(){
 | |
| 					var direction = that._snakes[color].direction
 | |
| 					var size = that._snakes[color].size
 | |
| 					var target = cell[direction]()
 | |
| 
 | |
| 					// skip a cell if it's already handled at this step.
 | |
| 					if(cell.o.moved_at == that._tick){
 | |
| 						return
 | |
| 					}
 | |
| 
 | |
| 					// do this only for the head...
 | |
| 					if(parseInt(cell.o.age) == size){
 | |
| 						// handle field bounds...
 | |
| 						if(target.o == null){
 | |
| 							alert('out of bounds!')
 | |
| 							return
 | |
| 						} 
 | |
| 						// kill conditions: walls and other snakes...
 | |
| 						if(that.is_snake(target) || that.is_wall(target)){
 | |
| 							// XXX move this to a separate action
 | |
| 							this.kill()
 | |
| 							return
 | |
| 						}
 | |
| 						// apple... 
 | |
| 						if(that.is_apple(target)){
 | |
| 							// grow the snake by one...
 | |
| 							// XXX move this to a separate action
 | |
| 							that._snakes[color].size += 1
 | |
| 							size = that._snakes[color].size
 | |
| 							if(that.on_apple_eaten != null){
 | |
| 								that.on_apple_eaten(that)
 | |
| 							}
 | |
| 						}
 | |
| 						// all clear, do the move...
 | |
| 						target.o.style.backgroundColor = color
 | |
| 						target.o.age = size
 | |
| 						target.o.moved_at = that._tick
 | |
| 						cell.o.age = size - 1
 | |
| 
 | |
| 					} else { 
 | |
| 						if(cell.o.age <= 1) {
 | |
| 							cell.o.style.backgroundColor = that.FIELD_COLOR
 | |
| 						}
 | |
| 						cell.o.age = parseInt(cell.o.age) - 1
 | |
| 					}
 | |
| 				},
 | |
| 
 | |
| 				// user interface...
 | |
| 				left: function(){
 | |
| 					that._snakes[color].direction = that._ccw[that._snakes[color].direction]
 | |
| 				},
 | |
| 				right: function(){
 | |
| 					that._snakes[color].direction = that._cw[that._snakes[color].direction]
 | |
| 				}
 | |
| 			})
 | |
| 		},
 | |
| 		is_snake: function(cell){
 | |
| 			var snakes = this._snakes
 | |
| 			var color = cell.o.style.backgroundColor
 | |
| 
 | |
| 			for(var c in snakes){
 | |
| 				if(c == color)
 | |
| 					return true
 | |
| 			}
 | |
| 			return false
 | |
| 		},
 | |
| 
 | |
| 		Apple: function(cell){
 | |
| 			cell.o.style.backgroundColor = this.APPLE_COLOR
 | |
| 			return cell
 | |
| 		},
 | |
| 		is_apple: function(cell){
 | |
| 			return cell.o.style.backgroundColor == this.APPLE_COLOR
 | |
| 		},
 | |
| 
 | |
| 		Wall: function(cell){
 | |
| 			cell.o.style.backgroundColor = this.WALL_COLOR
 | |
| 			return cell
 | |
| 		},
 | |
| 		is_wall: function(cell){
 | |
| 			return cell.o.style.backgroundColor == this.WALL_COLOR
 | |
| 		},
 | |
| 
 | |
| 		is_empty: function(cell){
 | |
| 			return cell.o.style.backgroundColor == this.FIELD_COLOR
 | |
| 		}
 | |
| 	}).init(field_id)
 | |
| }
 | |
| 
 | |
| // this defines the basic game logic and controls the rules and levels...
 | |
| /*
 | |
| 	NOTE: it is recommended to create game objects in the folowing order:
 | |
| 		1) walls
 | |
| 		2) apples
 | |
| 		3) players
 | |
|  */
 | |
| function JSSnakeGame(field){
 | |
| 
 | |
| 	var game = {
 | |
| 		field: field,
 | |
| 		TICK: 300,
 | |
| 
 | |
| 		// this enables snakes of the same colors...
 | |
| 		SIMILAR_COLORS: false,
 | |
| 		used_colors: function(){
 | |
| 			// this is a workaround the inability to directly create an object
 | |
| 			// with field names not a literal identifier or string...
 | |
| 			var res = {}
 | |
| 			res[field.FIELD_COLOR] = true
 | |
| 			res[field.WALL_COLOR] = true
 | |
| 			res[field.APPLE_COLOR] = true
 | |
| 			return res
 | |
| 		}(),
 | |
| 
 | |
| 		// utility methods...
 | |
| 		_random_empty_cell: function(){
 | |
| 			// NOTE: if we are really unlucky, this will take 
 | |
| 			//       really long, worse, if we are infinitely unlucky
 | |
| 			//       this will take an infinite amount of time... (almost)
 | |
| 			var field = this.field
 | |
| 			var i = field.cells.length-1
 | |
| 			var l = i
 | |
| 			while(true){
 | |
| 				var c = field.Cell(Math.round(Math.random()*l))
 | |
| 				if(field.is_empty(c))
 | |
| 					return c
 | |
| 				i--
 | |
| 				if(i == 0)
 | |
| 					return null
 | |
| 			}
 | |
| 		},
 | |
| 		// key handler...
 | |
| 		// functions:
 | |
| 		// 	- control snake - dispatch player-specific keys to player-specific snake
 | |
| 		// 	- create player - two unused keys pressed within timeout, random color 
 | |
| 		//
 | |
| 		// modes:
 | |
| 		// 	- player add
 | |
| 		// 	- game control
 | |
| 		//
 | |
| 		// NOTE: modes can intersect...
 | |
| 		// NOTE: modes are game state dependant...
 | |
| 		key_time_frame: 0.5,
 | |
| 		pending_key: null,
 | |
| 
 | |
| 		_keyHandler: function(evt){
 | |
| 			var name, color
 | |
| 			var key = window.event ? event.keyCode : evt.keyCode
 | |
| 
 | |
| 			// find a target registered for key...
 | |
| 			// XXX
 | |
| 
 | |
| 			// no one is registered...
 | |
| 			// if wait time set and is not exceeded create a player and register keys
 | |
| 			if(!this.pending_key || Date().getTime() - this.pending_key['time'] > this.key_time_frame ){
 | |
| 				// if no wait time is set, set it and remember the key...
 | |
| 				this.pending_key = {time: Date().getTime(), key: key}
 | |
| 			} else {
 | |
| 				// get name...
 | |
| 				// XXX
 | |
| 				// get color...
 | |
| 				// XXX
 | |
| 				this.Player(name, this.pending_key['key'], key, color)
 | |
| 				this.pending_key = null
 | |
| 			}
 | |
| 			return true
 | |
| 		},
 | |
| 		// create a new player...
 | |
| 		// NOTE: direction and position are optional...
 | |
| 		// XXX BUG: players should not get created facing a wall directly...
 | |
| 		Player: function(name, ccw_button, cw_button, color, cell, direction, size){
 | |
| 			if(!this.SIMILAR_COLORS && this.used_colors[color] == true){
 | |
| 				// error: that the color is already used...
 | |
| 				return
 | |
| 			}
 | |
| 			// register controls...
 | |
| 			// XXX 
 | |
| 
 | |
| 			if(direction == null){
 | |
| 				direction = ['n', 's', 'e', 'w'][Math.round(Math.random()*3)]
 | |
| 			}
 | |
| 			if(cell === null){
 | |
| 				cell = this._random_empty_cell()
 | |
| 				if(cell === null)
 | |
| 					return
 | |
| 			}
 | |
| 			// create a snake...
 | |
| 			this.used_colors[color] = true
 | |
| 			return this.field.Snake(color, cell, direction, size)
 | |
| 		},
 | |
| 		// NOTE: position is optional...
 | |
| 		Apple: function(cell){
 | |
| 			// place an apple at a random and not occupied position...
 | |
| 			var c = cell? cell: this._random_empty_cell()
 | |
| 			if(c === null)
 | |
| 				return
 | |
| 			return this.field.Apple(c)
 | |
| 			
 | |
| 		},
 | |
| 		// NOTE: all arguments are optional...
 | |
| 		Wall: function(cell, len, direction){
 | |
| 			// generate random data for arguments that are not given...
 | |
| 			if(cell == null){
 | |
| 				cell = this._random_empty_cell()
 | |
| 				if(cell === null)
 | |
| 					return
 | |
| 			}
 | |
| 			if(direction == null){
 | |
| 				direction = ['n', 's', 'e', 'w'][Math.round(Math.random()*3)]
 | |
| 			}
 | |
| 			if(len == null){
 | |
| 				if(direction == 'n' || direction == 's')
 | |
| 					var max = this.field.height 
 | |
| 				else
 | |
| 					var max = this.field.width 
 | |
| 				len = Math.round(Math.random()*(max-1))
 | |
| 			}
 | |
| 			// place a wall...
 | |
| 			for(var i=0; i < len; i++){
 | |
| 				field.Wall(cell)
 | |
| 				cell = cell[direction]()
 | |
| 			}
 | |
| 		},
 | |
| 
 | |
| 		// level generators and helpers...
 | |
| 		levels: {
 | |
| 			dotted: function(n){
 | |
| 				for(var i=0; i < n; i++)
 | |
| 					game.Wall(null, 1, null)
 | |
| 			},
 | |
| 			dashed: function(n, length){
 | |
| 				if(length == null)
 | |
| 					length = 3
 | |
| 				for(var i=0; i < n; i++)
 | |
| 					game.Wall(null, length, null)
 | |
| 			},
 | |
| 			// specific level styles...
 | |
| 			sand: function(){
 | |
| 				this.dotted(Math.round(game.field.cells.length/20))
 | |
| 			},
 | |
| 			walls: function(){
 | |
| 				this.dashed(
 | |
| 					Math.round(game.field.cells.length/90), 
 | |
| 					Math.min(
 | |
| 						game.field.width, 
 | |
| 						game.field.height)-2)
 | |
| 			}
 | |
| 		},
 | |
| 
 | |
| 		start: function(){
 | |
| 			// start the game...
 | |
| 			field.start(this.TICK)
 | |
| 		},
 | |
| 		stop: function(){
 | |
| 			field.stop()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	field.on_apple_eaten = function(){game.Apple()}
 | |
| 	field.on_kill = function(snake){game.used_colors[snake.color] = false}
 | |
| 	
 | |
| 	//document.onkeyup = function(evt){return game._keyHandler(evt)}
 | |
| 
 | |
| 	return game
 | |
| }
 | |
| 
 | |
| // vim:set ts=4 sw=4 spell :
 |