Alex A. Naanou 0adde2cbdf added jsssnake sources...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2013-03-13 08:09:05 +04:00

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 :