├── .gitignore ├── publish-demo ├── audio ├── fire.ogg ├── intro1.ogg ├── intro2.ogg ├── join1.ogg ├── join2.ogg ├── move1.ogg ├── move2.ogg ├── move3.ogg └── explosion.ogg ├── images ├── explosion.png ├── man-left-1.png ├── man-left-2.png ├── man2-large.png ├── burwor-left-1.png ├── burwor-left-2.png ├── garwor-left-1.png ├── garwor-left-2.png ├── garwor-left-3.png ├── wizard-of-wor.png ├── thorwor-left-1.png ├── thorwor-left-2.png └── thorwor-left-3.png ├── backlog.txt ├── README.md ├── index.html ├── vector.js ├── lib ├── Bacon.js └── underscore.js └── worzone.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /publish-demo: -------------------------------------------------------------------------------- 1 | scp -r * juhapajuha@secure.summitmedia.fi:web/worzone 2 | -------------------------------------------------------------------------------- /audio/fire.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/fire.ogg -------------------------------------------------------------------------------- /audio/intro1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/intro1.ogg -------------------------------------------------------------------------------- /audio/intro2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/intro2.ogg -------------------------------------------------------------------------------- /audio/join1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/join1.ogg -------------------------------------------------------------------------------- /audio/join2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/join2.ogg -------------------------------------------------------------------------------- /audio/move1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/move1.ogg -------------------------------------------------------------------------------- /audio/move2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/move2.ogg -------------------------------------------------------------------------------- /audio/move3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/move3.ogg -------------------------------------------------------------------------------- /audio/explosion.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/audio/explosion.ogg -------------------------------------------------------------------------------- /images/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/explosion.png -------------------------------------------------------------------------------- /images/man-left-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/man-left-1.png -------------------------------------------------------------------------------- /images/man-left-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/man-left-2.png -------------------------------------------------------------------------------- /images/man2-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/man2-large.png -------------------------------------------------------------------------------- /images/burwor-left-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/burwor-left-1.png -------------------------------------------------------------------------------- /images/burwor-left-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/burwor-left-2.png -------------------------------------------------------------------------------- /images/garwor-left-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/garwor-left-1.png -------------------------------------------------------------------------------- /images/garwor-left-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/garwor-left-2.png -------------------------------------------------------------------------------- /images/garwor-left-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/garwor-left-3.png -------------------------------------------------------------------------------- /images/wizard-of-wor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/wizard-of-wor.png -------------------------------------------------------------------------------- /images/thorwor-left-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/thorwor-left-1.png -------------------------------------------------------------------------------- /images/thorwor-left-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/thorwor-left-2.png -------------------------------------------------------------------------------- /images/thorwor-left-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/worzone/HEAD/images/thorwor-left-3.png -------------------------------------------------------------------------------- /backlog.txt: -------------------------------------------------------------------------------- 1 | Bugs 2 | 3 | - Runs slow in Firefox 4 | - Level does not get cleared in FF 5 | - Scores, Lives displays are reseted on each level 6 | - Sometimes man figure is detached from actual location 7 | - Monsters should be removed on Game Over too 8 | 9 | Features 10 | 11 | - Start screen with ASCII graphics 12 | - Game Over with ASCII graphics 13 | - more and eviler monsters for each level 14 | - better monster logic (garwor to follow players?) 15 | - player should be ejected from the starting point in 10 seconds and should not be able to go back 16 | - thorwors and whatnot 17 | 18 | Improvements 19 | 20 | - Animation for "man shooting" 21 | - Alpha channel for PNGs 22 | - PNGs to SVGs 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Worzone 2 | ======= 3 | 4 | This simple game, strongly inspired by the C64 classic Wizard Of Wor, is 5 | an experiment on using Reactive Functional Programming on a game. I did 6 | this first with RxJs (Reactive Extensions of Javascript) but lately I 7 | wrote my own FRP framework, [bacon.js](http://github.com/raimohanska/bacon.js), 8 | and converted Worzone to use that instead. 9 | 10 | See the [online demo](https://raimohanska.github.io/worzone/). 11 | 12 | I have described the basic ideas in my blog posting [Game Programming With 13 | RxJs](http://nullzzz.blogspot.com/2011/02/game-programming-with-rx-js.html). 14 | 15 | The switch from RxJs to Bacon.js did not dramatically change the 16 | internal structure of the game, except for some simplification made 17 | possible by including stuff like Bus in the framework itself. 18 | Performance seems to be pretty much the same, too. Profiling with Chrome 19 | shows that most CPU time is wasted in Raphaël rendering, so maybe I 20 | (or you) should try replacing that with HTML canvas. 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /vector.js: -------------------------------------------------------------------------------- 1 | function Point(x, y) { 2 | return new Vector2D(x, y) 3 | } 4 | 5 | // Number -> Number -> Vector2D 6 | function Vector2D(x, y) { 7 | this.x = x; 8 | this.y = y; 9 | } 10 | 11 | Vector2D.prototype = { 12 | // Vector2D -> Vector2D 13 | add : function(other) { return new Vector2D(this.x + other.x, this.y + other.y) }, 14 | // Vector2D -> Vector2D 15 | subtract : function(other) { return new Vector2D(this.x - other.x, this.y - other.y) }, 16 | // Unit -> Number 17 | getLength : function() { return Math.sqrt(this.x * this.x + this.y * this.y) }, 18 | // Number -> Vector2D 19 | times : function(multiplier) { return new Vector2D(this.x * multiplier, this.y * multiplier) }, 20 | // Unit -> Vector2D 21 | invert : function() { return new Vector2D(-this.x, -this.y) }, 22 | // Number -> Vector2D 23 | withLength : function(newLength) { return this.times(newLength / this.getLength()) }, 24 | rotateRad : function(radians) { 25 | var length = this.getLength() 26 | var currentRadians = this.getAngle() 27 | var resultRadians = radians + currentRadians 28 | var rotatedUnit = new Vector2D(Math.cos(resultRadians), Math.sin(resultRadians)) 29 | return rotatedUnit.withLength(length) 30 | }, 31 | // Number -> Vector2D 32 | rotateDeg : function(degrees) { 33 | var radians = degrees * 2 * Math.PI / 360 34 | return this.rotateRad(radians) 35 | }, 36 | // Unit -> Number 37 | getAngle : function() { 38 | var length = this.getLength() 39 | unit = this.withLength(1) 40 | return Math.atan2(unit.y, unit.x) 41 | }, 42 | getAngleDeg : function() { 43 | return this.getAngle() * 360 / (2 * Math.PI) 44 | }, 45 | floor : function() { 46 | return new Vector2D(Math.floor(this.x), Math.floor(this.y)) 47 | }, 48 | toString : function() { 49 | return "(" + x + ", " + y + ")" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/Bacon.js: -------------------------------------------------------------------------------- 1 | ((function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E=this,F=Object.prototype.hasOwnProperty,G=function(a,b){function d(){this.constructor=a}for(var c in b)F.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a},H=function(a,b){return function(){return a.apply(b,arguments)}};(D=this.jQuery||this.Zepto)!=null&&(D.fn.asEventStream=function(b){var c;return c=this,new g(function(d){var e,f;return e=function(b){var c;c=d(y(b));if(c===a.noMore)return f()},f=function(){return c.unbind(b,e)},c.bind(b,e),f})}),a=this.Bacon={taste:"delicious"},a.noMore="veggies",a.more="moar bacon!",a.never=function(){return new g(function(a){return function(){return z}})},a.later=function(b,c){return a.sequentially(b,[c])},a.sequentially=function(b,c){return a.repeatedly(b,c).take(r(function(a){return a.hasValue()},x(C,c)).length)},a.repeatedly=function(b,c){var d,e;return d=-1,e=function(){return d++,C(c[d%c.length])},a.fromPoll(b,e)},a.fromPoll=function(b,c){return new g(function(d){var e,f,g;return f=void 0,e=function(){var b,e;e=c(),b=d(e);if(b===a.noMore||e.isEnd())return g()},g=function(){return clearInterval(f)},f=setInterval(e,b),g})},a.interval=function(b,c){var d;return c==null&&(c={}),d=function(){return y(c)},a.fromPoll(b,d)},a.constant=function(a){return new k(function(b){return b(u(a)),b(q()),z})},a.combineAll=function(a,b){var c,d,e,f,g;d=t(a),g=B(a);for(e=0,f=g.length;e0?(b--,a.more):this.push(c):this.push(c)})},b.prototype.distinctUntilChanged=function(){return this.withStateMachine(void 0,function(a,b){return b.hasValue()?a!==b.value?[b.value,[b]]:[a,[]]:[a,[b]]})},b.prototype.withStateMachine=function(b,c){var d;return d=b,this.withHandler(function(b){var e,f,g,h,i,j,k;e=c(d,b),f=e[0],h=e[1],d=f,i=a.more;for(j=0,k=h.length;j0},g=z,d=function(a){return A(a,e)},this.push=function(b){var c,g,i,j,k,l,n;j=void 0,c=function(){var a,c,d,e;if(j!=null){c=j,j=void 0;for(d=0,e=c.length;d=0)return b.splice(c,1)},o=function(a,b){return a.indexOf(b)>=0}})).call(this); -------------------------------------------------------------------------------- /worzone.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var bounds = Rectangle(0, 0, 500, 450) 3 | var r = Raphael(20, 20, bounds.width, bounds.height); 4 | var messageQueue = new Bacon.Bus() 5 | messageQueue.ofType = function(type) { 6 | return messageQueue.filter(function(message) { 7 | return message.message == type 8 | }) 9 | } 10 | var targets = Targets(messageQueue) 11 | 12 | Monsters(messageQueue, targets, r) 13 | Players(messageQueue, targets, r) 14 | 15 | var audio = Audio() 16 | GameSounds(messageQueue, audio) 17 | 18 | $('#sound').click(function() { audio.toggle() }) 19 | 20 | Levels(messageQueue, targets, r) 21 | }) 22 | 23 | function Levels(messageQueue, targets, r) { 24 | var gameOver = messageQueue.ofType("gameover").skip(1) 25 | var levelFinished = messageQueue.ofType("level-finished") 26 | var startGame = Keyboard().anyKey.take(1) 27 | var startScreen = AsciiGraphic(startScreenData(), 7, 7, Point(50, 150)).render(r).attr({ fill : "#F00"}) 28 | startGame.onValue(removeElements(startScreen)) 29 | 30 | var levelStarting = levelFinished 31 | .merge(startGame) 32 | .scan(0, function(prev, _) { return prev + 1 }) 33 | .map(function(level) { return { message : "level-starting", level : level} }) 34 | .changes() 35 | var levels = levelStarting 36 | .delay(4000) 37 | .map(function(level) { 38 | var levelEnd = levelFinished.merge(gameOver) 39 | return { message : "level-started", level : level.level, maze : Maze(level.level), levelEnd : levelEnd } 40 | }) 41 | levelStarting.onValue(function() { 42 | var getReady = AsciiGraphic(getReadyData(), 7, 7, Point(30, 80)).render(r) 43 | setTimeout(function() { 44 | var go = AsciiGraphic(goData(), 7, 7, Point(200, 170)).render(r) 45 | setTimeout(removeElements([getReady, go]), 2000) 46 | }, 2000) 47 | }) 48 | 49 | levels.onValue(function(level) { 50 | level.maze.draw(level.levelEnd, r) 51 | var pos = level.maze.levelNumberPos() 52 | var text = r.text(pos.x, pos.y, "Level " + level.level).attr({ fill : "#FF0"}) 53 | level.levelEnd.onValue(removeElements(text)) 54 | }) 55 | 56 | levels.toProperty().sampledBy(gameOver).onValue(function(level){ 57 | var pos = level.maze.centerMessagePos() 58 | r.text(pos.x, pos.y, "GAME OVER").attr({ fill : "#f00", "font-size" : 50, "font-family" : "courier"}) 59 | }) 60 | 61 | messageQueue.ofType("fire").onValue(function(fire) { 62 | function targetFilter(target) { 63 | if (fire.shooter.monster && target.monster) return false; 64 | return fire.shooter != target 65 | } 66 | Bullet(fire.pos, fire.shooter, fire.dir, fire.maze, targets, targetFilter, messageQueue, r) 67 | }) 68 | 69 | messageQueue.plug(levels) 70 | messageQueue.plug(levelStarting) 71 | } 72 | 73 | function GameSounds(messageQueue, audio) { 74 | function sequence(delay, count) { 75 | return ticker(delay).scan(1, function(counter, _) { return counter % count + 1} ).changes() 76 | } 77 | levelStarted = messageQueue.ofType("level-started").map(always(true)) 78 | .merge(messageQueue.ofType("level-finished").map(always(false))) 79 | .toProperty(false) 80 | levelCurrentlyPlayed = Bacon.latestValue(levelStarted) 81 | sequence(500, 3) 82 | .filter(levelCurrentlyPlayed) 83 | .onValue(function(counter) { audio.playSound("move" + counter)() }) 84 | messageQueue.ofType("start") 85 | .filter(function (start) { return start.object.player }) 86 | .map(function(start) { return start.object.player.id }) 87 | .onValue(function(id) { audio.playSound("join" + id)() }) 88 | messageQueue.ofType("fire").onValue(audio.playSound("fire")) 89 | messageQueue.ofType("hit").onValue(audio.playSound("explosion")) 90 | messageQueue.ofType("level-starting").onValue(audio.playSound("intro1")) 91 | } 92 | 93 | function Audio() { 94 | var on = false 95 | var sounds = {} 96 | 97 | function loadSound(soundName) { 98 | var audioElement = document.createElement('audio') 99 | audioElement.setAttribute('src', "audio/" + soundName + ".ogg") 100 | return audioElement 101 | } 102 | 103 | function getSound(soundName) { 104 | if (!sounds[soundName]) { 105 | sounds[soundName] = loadSound(soundName) 106 | } 107 | return sounds[soundName] 108 | } 109 | function play(soundName) { 110 | if (on) getSound(soundName).play() 111 | } 112 | return { 113 | playSound : function(soundName) { return function() { play(soundName) }}, 114 | toggle : function() { on = !on; } 115 | } 116 | } 117 | 118 | function Players(messageQueue, targets, r) { 119 | var player1 = Player(1, KeyMap([[87, up], [83, down], [65, left], [68, right]], [70]), targets, messageQueue, r) 120 | var player2 = Player(2, KeyMap([[38, up], [40, down], [37, left], [39, right]], [189, 109, 18]), targets, messageQueue, r) 121 | } 122 | 123 | function Monsters(messageQueue, targets, r) { 124 | messageQueue.ofType("level-started").onValue(function(level) { 125 | var maze = level.maze 126 | function burwor() { Burwor(level.level * 0.5 + 1, maze, messageQueue, targets, r) } 127 | function garwor() { Garwor(level.level * 0.5 + 1, maze, messageQueue, targets, r) } 128 | _.range(0, 5).forEach(burwor) 129 | var monsterHit = messageQueue.ofType("hit") 130 | .filter(function (hit) { return hit.target.monster }) 131 | var levelFinished = monsterHit 132 | .skip(15) 133 | .map(always({ message : "level-finished"})) 134 | .take(1) 135 | monsterHit 136 | .delay(2000) 137 | .takeUntil(levelFinished) 138 | .onValue(garwor) 139 | ticker(5000) 140 | .takeUntil(levelFinished) 141 | .filter(function() { return (targets.count(Monsters.monsterFilter) < 10) }) 142 | .onValue(burwor) 143 | messageQueue.plug(levelFinished) 144 | }) 145 | } 146 | Monsters.monsterFilter = function(target) { return target.monster } 147 | 148 | function KeyMap(directionKeyMap, fireKey) { 149 | return { 150 | directionKeyMap : directionKeyMap, 151 | fireKey : fireKey 152 | } 153 | } 154 | 155 | function Player(id, keyMap, targets, messageQueue, r) { 156 | var player = { 157 | id : id, 158 | keyMap : keyMap, 159 | toString : function() { return "Player " + id} 160 | } 161 | var startLives = 3 162 | var death = messageQueue.ofType("hit") 163 | .filter(function (hit) { return hit.target.player == player }) 164 | var lives = death 165 | .scan(startLives, function(lives, hit) { return lives - 1 }) 166 | .map( function(lives) { return { message : "lives", player : player, lives : lives}}) 167 | var gameOver = lives.changes() 168 | .filter(function(lives) { return lives.lives == 0}) 169 | .map(function() { return { message : "gameover", player : player} } ) 170 | var joinMessage = { message : "join", player : player} 171 | var levelStart = messageQueue.ofType("level-started") 172 | var join = death 173 | .merge(levelStart) 174 | .takeUntil(gameOver) 175 | .map(always(joinMessage)) 176 | Score(player, messageQueue, r) 177 | LivesDisplay(player, lives, messageQueue, r) 178 | levelStart.toProperty().sampledBy(join).onValue(function(level) { PlayerFigure(player, level.maze, messageQueue, targets, r) }) 179 | messageQueue.plug(join) 180 | messageQueue.plug(lives) 181 | messageQueue.plug(gameOver) 182 | return player; 183 | } 184 | 185 | function LivesDisplay(player, lives, messageQueue, r) { 186 | messageQueue.ofType("level-started") 187 | .decorateWith("lives", lives).onValue(function(level) { 188 | var pos = level.maze.playerScorePos(player) 189 | _.range(0, level.lives.lives - 1).forEach(function(index) { 190 | var image = PlayerImage(player).create(pos.add(Point(index * 20, 10)), 8, r) 191 | lives.changes() 192 | .filter(function(lives) { return lives.lives <= index + 1}) 193 | .merge(level.levelEnd) 194 | .onValue(removeElements(image)) 195 | }) 196 | }) 197 | } 198 | 199 | function Score(player, messageQueue, r) { 200 | var score = messageQueue.ofType("hit") 201 | .filter(function(hit) { return hit.shooter && hit.shooter.player == player} ) 202 | .map(function(hit) { return hit.target.points }) 203 | .scan(0, function(current, delta) { return current + delta }) 204 | messageQueue.plug(score.map(function(points) { return { message : "score", player : player, score : points} } )) 205 | messageQueue.ofType("level-started").decorateWith("score", score).onValue(function(level){ 206 | var pos = level.maze.playerScorePos(player) 207 | var scoreDisplay = r.text(pos.x, pos.y - 10, level.score).attr({ fill : "#ff0"}) 208 | score.takeUntil(level.levelEnd).onValue(function(points) { scoreDisplay.attr({ text : points }) }) 209 | level.levelEnd.onValue(function(){ scoreDisplay.remove() }) 210 | }) 211 | } 212 | 213 | function ControlInput(directionInput, fireInput) { 214 | return {directionInput : directionInput, fireInput : fireInput} 215 | } 216 | 217 | function Targets(messageQueue) { 218 | var targets = [] 219 | messageQueue.ofType("remove").onValue(function(remove) { 220 | targets = _.select(targets, function(target) { return target != remove.object}) 221 | }) 222 | messageQueue.ofType("create").onValue(function(create) { 223 | targets.push(create.target) 224 | }) 225 | function targetThat(predicate) { 226 | return first(_.select(targets, predicate)) 227 | } 228 | return { 229 | hit : function(pos, filter) { return this.inRange(pos, 0, filter) }, 230 | inRange : function(pos, range, filter) { return targetThat(function(target) { 231 | return target.inRange(pos, range) && filter(target) })}, 232 | byId : function(id) { return targetThat(function(target) { return target.id == id })}, 233 | count : function(filter) { return _.select(targets, filter).length}, 234 | select : function(filter) { return _.select(targets, filter) } 235 | } 236 | } 237 | 238 | function Bullet(startPos, shooter, velocity, maze, targets, targetFilter, messageQueue, r) { 239 | var radius = 3 240 | var bullet = r.circle(startPos.x, startPos.y, radius).attr({fill: "#f00"}) 241 | bullet.radius = radius 242 | var movements = gameTicker.multiply(20).map(function(_) {return velocity}) 243 | var unlimitedPosition = movements 244 | .scan(startPos, function(pos, move) { return pos.add(move) }) 245 | var collision = unlimitedPosition.changes().filter(function(pos) { return !maze.isAccessible(pos, radius, radius) }).take(1) 246 | var hit = unlimitedPosition.changes() 247 | .filter(function(pos) { return targets.hit(pos, targetFilter) }) 248 | .map(function(pos) { return { message : "hit", target : targets.hit(pos, targetFilter), shooter : shooter}}) 249 | .take(1) 250 | .takeUntil(collision) 251 | var hitOrCollision = collision.merge(hit) 252 | var position = unlimitedPosition.sampledBy(gameTicker).takeUntil(hitOrCollision) 253 | 254 | position.onValue(function (pos) { bullet.animate({cx : pos.x, cy : pos.y}, delay) }) 255 | hitOrCollision.onValue(function(pos) { bullet.remove() }) 256 | messageQueue.plug(hit) 257 | } 258 | 259 | function PlayerImage(player) { 260 | return FigureImage("man", 2, 2) 261 | } 262 | 263 | function PlayerFigure(player, maze, messageQueue, targets, r) { 264 | var directionInput = Keyboard().multiKeyState(player.keyMap.directionKeyMap).map(first) 265 | var fireInput = Keyboard().keyDowns(player.keyMap.fireKey) 266 | var controlInput = ControlInput(directionInput, fireInput) 267 | var startPos = maze.playerStartPos(player) 268 | function access(pos) { return maze.isAccessible(pos, 16) } 269 | var man = Figure(startPos, PlayerImage(player), controlInput, maze, access, messageQueue, r) 270 | man.player = player 271 | man.points = 1000 272 | var hitByMonster = man.streams.position 273 | .sampledBy(gameTicker) 274 | .filter(function(status) { return targets.inRange(status.pos, man.radius, Monsters.monsterFilter) }) 275 | .map(function(pos) { return { message : "hit", target : man}}) 276 | .take(1) 277 | messageQueue.plug(hitByMonster) 278 | return man 279 | } 280 | 281 | function FigureImage(imgPrefix, animCount, animCycle) { 282 | imgPrefix = imgPath + imgPrefix 283 | function flip(img, f) { 284 | var x = img.attrs.x, 285 | y = img.attrs.y; 286 | img.scale(f, 1); 287 | img.attr({x:x, y:y}); 288 | } 289 | function rotate(img, absoluteRotation) { 290 | img.rotate(absoluteRotation, img.attrs.x + img.attrs.width/2, img.attrs.y + img.attrs.height/2); 291 | } 292 | return { 293 | create : function(startPos, radius, r) { 294 | return r.image(imgPrefix + "-left-1.png", startPos.x - radius, startPos.y - radius, radius * 2, radius * 2) 295 | }, 296 | animate : function(figure, status) { 297 | var animationSequence = status.changes().bufferWithCount(animCycle).scan(1, function(prev, _) { return prev % animCount + 1}) 298 | var animation = status.combine(animationSequence, function(status, index) { 299 | return { image : imgPrefix + "-left-" + index + ".png", dir : status.dir } 300 | }) 301 | animation.onValue(function(anim) { 302 | if(figure.removed) return; 303 | figure.attr({src : anim.image}) 304 | if(anim.dir == left) { 305 | // when facing left, use the pic as is 306 | flip(figure, 1) 307 | rotate(figure, 0) 308 | } else { 309 | // when facing any other way, flip the pic and then rotate it 310 | flip(figure, -1) 311 | rotate(figure, anim.dir.getAngleDeg()) 312 | } 313 | }) 314 | } 315 | } 316 | } 317 | 318 | function Burwor(speed, maze, messageQueue, targets, r) { 319 | return Monster(speed, FigureImage("burwor", 2, 10), 100, 5000, maze, messageQueue, targets, r) 320 | } 321 | 322 | function Garwor(speed, maze, messageQueue, targets, r) { 323 | return Monster(speed, FigureImage("garwor", 3, 6), 200, 2000, maze, messageQueue, targets, r) 324 | } 325 | 326 | function Monster(speed, image, points, fireInterval, maze, messageQueue, targets, r) { 327 | var fire = ticker(fireInterval).filter( function() { return Math.random() < 0.1 }) 328 | var direction = new Bacon.Bus() 329 | function access(pos) { return maze.isAccessibleByMonster(pos, 16) } 330 | var startPos = maze.randomFreePos(function(pos) { 331 | return access(pos) && targets.select(function(target){ return target.player && target.inRange(pos, 100) }).length == 0 332 | }) 333 | var monster = Figure(startPos, image, ControlInput(direction, fire), maze, access, messageQueue, r) 334 | monster.speed = speed 335 | monster.monster = true 336 | monster.points = points 337 | direction.plug(monster.streams.position.sampledBy(gameTicker).scan(left, function(current, status) { 338 | function canMove(dir) { return access(status.pos.add(dir)) } 339 | if (canMove(current)) return current 340 | var possible = _.select([left, right, up, down], canMove) 341 | return possible[randomInt(possible.length)] 342 | })) 343 | } 344 | 345 | function Movement(figure, access) { 346 | function moveIfPossible(pos, direction, speed) { 347 | if (speed == undefined) speed = figure.speed 348 | if (speed <= 0) return pos 349 | var nextPos = pos.add(direction.times(speed)) 350 | if (!access(nextPos, figure.radius)) 351 | return moveIfPossible(pos, direction, speed -1) 352 | return nextPos 353 | } 354 | 355 | return { 356 | moveIfPossible: moveIfPossible 357 | } 358 | } 359 | 360 | function Figure(startPos, image, controlInput, maze, access, messageQueue, r) { 361 | var radius = 16 362 | var figure = image.create(startPos, radius, r) 363 | figure.radius = radius 364 | figure.speed = 4 365 | var hit = messageQueue.ofType("hit").filter(function(hit) { return hit.target == figure }).take(1) 366 | var levelFinished = messageQueue.ofType("level-finished").take(1) 367 | var removed = hit.merge(levelFinished).take(1).map(always({ message : "remove", object : figure})) 368 | 369 | var direction = controlInput.directionInput.takeUntil(removed) 370 | var latestDirection = direction.filter(identity).toProperty(left) 371 | var movements = direction.toProperty().sampledBy(gameTicker).filter(identity).takeUntil(removed) 372 | var position = movements.scan(startPos, Movement(figure, access).moveIfPossible) 373 | 374 | position.onValue(function (pos) { figure.attr({x : pos.x - radius, y : pos.y - radius}) }) 375 | hit.onValue(function() { 376 | // TODO: fix timing issue : shouldn't have to delay before gif change 377 | setTimeout(function(){ figure.attr({src : imgPath + "explosion.png"}) }, 100) 378 | setTimeout(function(){ figure.remove() }, 1000) 379 | }) 380 | levelFinished.onValue(function() { figure.remove() }) 381 | 382 | var status = position.combine(latestDirection, function(pos, dir) { 383 | return { message : "move", object : figure, pos : pos, dir : dir } 384 | }) 385 | 386 | image.animate(figure, status) 387 | 388 | var fire = status.sampledBy(controlInput.fireInput).map(function(status) { 389 | return { message : "fire", pos : status.pos.add(status.dir.withLength(radius + 5)), 390 | dir : status.dir, shooter : figure, maze : maze, 391 | } 392 | }).takeUntil(removed) 393 | 394 | var start = movements.take(1).map(function() { return { message : "start", object : figure} }) 395 | messageQueue.plug(start) 396 | messageQueue.plug(fire) 397 | messageQueue.plug(removed) 398 | var currentPosition = Bacon.latestValue(position) 399 | figure.inRange = function(pos, range) { return currentPosition().subtract(pos).getLength() < range + radius } 400 | messageQueue.push({ message : "create", target : figure }) 401 | figure.streams = { 402 | position : status 403 | } 404 | return figure 405 | } 406 | 407 | function Keyboard() { 408 | var allKeyUps = $(document).asEventStream("keyup") 409 | var allKeyDowns = $(document).asEventStream("keydown") 410 | //allKeyDowns.onValue(function(event) {console.log(event.keyCode)}) 411 | function keyCodeIs(keyCode) { return function(event) { return event.keyCode == keyCode} } 412 | function keyCodeIsOneOf(keyCodes) { return function(event) { return keyCodes.indexOf(event.keyCode) >= 0} } 413 | function keyUps(keyCode) { return allKeyUps.filter(keyCodeIs(keyCode)) } 414 | function keyDowns(keyCodes) { 415 | return allKeyDowns.filter(keyCodeIsOneOf(toArray(keyCodes))) 416 | } 417 | function keyState(keyCode, value) { 418 | return keyDowns(keyCode).map(always([value])). 419 | merge(keyUps(keyCode).map(always([]))).toProperty([]).distinctUntilChanged() 420 | } 421 | function multiKeyState(keyMap) { 422 | var streams = keyMap.map(function(pair) { return keyState(pair[0], pair[1]) }) 423 | return Bacon.combineAsArray(streams) 424 | } 425 | return { 426 | multiKeyState : multiKeyState, 427 | keyDowns : keyDowns, 428 | anyKey : allKeyDowns 429 | } 430 | } 431 | 432 | var mazes = [ 433 | [ "*******************", 434 | "* *", 435 | "* ******* ****** *", 436 | "* * * *", 437 | "* * ******* * *", 438 | "* * * * * *", 439 | "* * * *", 440 | "* C *", 441 | "* * ******* * *", 442 | "* * * *", 443 | "* * * *", 444 | "* * * *", 445 | "* ******* ****** *", 446 | "* *", 447 | "* *************** *", 448 | "*1*5XXXXXLXXXX60*2*", 449 | "***XXXXXXXXXXXXX***" ], 450 | 451 | [ "*******************", 452 | "* *", 453 | "* ******* ****** *", 454 | "* * * *", 455 | "* * * ******* * * *", 456 | "* * * * * * * *", 457 | "* * * * * * * *", 458 | "* * * C * *", 459 | "* * * *** *** *****", 460 | "* * * * * * * *", 461 | "* * * * * * * * * *", 462 | "* * * * *", 463 | "***** *** * ***** *", 464 | "* * * *", 465 | "* *************** *", 466 | "*1*5XXXXXLXXXX60*2*", 467 | "***XXXXXXXXXXXXX***" ] 468 | ] 469 | 470 | 471 | function Maze(level) { 472 | var data = mazes[(level + 1) % 2] 473 | var blockSize = 50 474 | var wall = 5 475 | var ascii = AsciiGraphic(data, blockSize, wall) 476 | 477 | function isWall(blockPos) { return ascii.isChar(blockPos, "*") } 478 | function isFree(blockPos) { return ascii.isChar(blockPos, "C ") } 479 | 480 | function findMazePos(character) { 481 | function blockThat(predicate) { 482 | return ascii.forEachBlock(function(blockPos) { 483 | if (predicate(blockPos)) { return blockPos} 484 | }) 485 | } 486 | return blockThat(function(blockPos) { return ascii.isChar(blockPos, character)}) 487 | } 488 | 489 | function accessible(pos, objectRadiusX, objectRadiusY, predicate) { 490 | if (!objectRadiusY) objectRadiusY = objectRadiusX 491 | var radiusX = objectRadiusX 492 | var radiusY = objectRadiusY 493 | for (var x = ascii.toBlockX(pos.x - radiusX); x <= ascii.toBlockX(pos.x + radiusX); x++) 494 | for (var y = ascii.toBlockY(pos.y - radiusY); y <= ascii.toBlockY(pos.y + radiusY); y++) 495 | if (!predicate(Point(x, y))) return false 496 | return true 497 | } 498 | return { 499 | levelNumberPos : function() { 500 | return ascii.blockCenter(findMazePos("L")) 501 | }, 502 | centerMessagePos : function() { 503 | return ascii.blockCenter(findMazePos("C")) 504 | }, 505 | playerStartPos : function(player) { 506 | return ascii.blockCenter(findMazePos("" + player.id)) 507 | }, 508 | playerScorePos : function(player) { 509 | var number = Number(player.id) + 4 510 | return ascii.blockCenter(findMazePos("" + number)) 511 | }, 512 | isAccessible : function(pos, objectRadiusX, objectRadiusY) { 513 | return accessible(pos, objectRadiusX, objectRadiusY, function(blockPos) { return !isWall(blockPos) }) 514 | }, 515 | isAccessibleByMonster : function(pos, objectRadiusX, objectRadiusY) { 516 | return accessible(pos, objectRadiusX, objectRadiusY, function(blockPos) { return isFree(blockPos) }) 517 | }, 518 | randomFreePos : function(filter) { 519 | while(true) { 520 | var pixelPos = ascii.blockCenter(ascii.randomBlock()) 521 | if (filter(pixelPos)) return pixelPos 522 | } 523 | }, 524 | draw : function(levelEnd, raphael) { 525 | var elements = ascii.renderWith(raphael, function(block) { 526 | if (isWall(block)) { 527 | var corner = ascii.blockCorner(block) 528 | var size = ascii.sizeOf(block) 529 | return raphael.rect(corner.x, corner.y, size.x, size.y).attr({ stroke : "#008", fill : "#008"}) 530 | } 531 | }) 532 | levelEnd.onValue(function() { 533 | elements.remove() 534 | }) 535 | } 536 | } 537 | } 538 | 539 | function AsciiGraphic(data, blockSize, wall, position) { 540 | if (!wall) wall = blockSize 541 | if (!position) position = Point(0, 0) 542 | var width = data[0].length 543 | var height = data.length 544 | var fullBlock = blockSize + wall 545 | 546 | function charAt(blockPos) { 547 | if (blockPos.y >= height || blockPos.x >= width || blockPos.x < 0 || blockPos.y < 0) return "X" 548 | return data[blockPos.y][blockPos.x] 549 | } 550 | function isChar(blockPos, chars) { 551 | return chars.indexOf(charAt(blockPos)) >= 0 552 | } 553 | function isWall(blockPos) { return isChar(blockPos, "*") } 554 | function isFree(blockPos) { return isChar(blockPos, "C ") } 555 | function blockCorner(blockPos) { 556 | function blockToPixel(block) { 557 | var fullBlocks = Math.floor(block / 2) 558 | return fullBlocks * fullBlock + ((block % 2 == 1) ? wall : 0) 559 | } 560 | return Point(blockToPixel(blockPos.x) + position.x, blockToPixel(blockPos.y) + position.y) 561 | } 562 | function blockCenter(blockPos) { 563 | return blockCorner(blockPos).add(sizeOf(blockPos).times(.5)) 564 | } 565 | function sizeOf(blockPos) { 566 | function size(x) { return ( x % 2 == 0) ? wall : blockSize} 567 | return Point(size(blockPos.x), size(blockPos.y)) 568 | } 569 | function toBlock(x) { 570 | var fullBlocks = Math.floor(x / fullBlock) 571 | var remainder = x - (fullBlocks * fullBlock) 572 | var wallToAdd = ((remainder >= wall) ? 1 : 0) 573 | return fullBlocks * 2 + wallToAdd 574 | } 575 | function toBlockX(x) { 576 | return toBlock(x - position.x) 577 | } 578 | function toBlockY(y) { 579 | return toBlock(y - position.y) 580 | } 581 | function toBlocks(pixelPos) { 582 | return Point(toBlockX(pixelPos.x), toBlockY(pixelPos.y)) 583 | } 584 | function forEachBlock(fn) { 585 | for (var x = 0; x < width; x++) { 586 | for (var y = 0; y < height; y++) { 587 | var result = fn(Point(x, y)) 588 | if (result) 589 | return result; 590 | } 591 | } 592 | } 593 | function randomBlock() { 594 | return Point(randomInt(width), randomInt(height)) 595 | } 596 | function render(r) { 597 | return renderWith(r, function(block) { 598 | if (!isChar(block, " ")) 599 | return r.text(blockCenter(block).x, blockCenter(block).y, charAt(block)).attr({ fill : "#f00", "font-family" : "Courier New, Courier", "font-size" : blockSize * 1.7}) 600 | }) 601 | } 602 | function renderWith(r, blockRenderer) { 603 | var elements = r.set() 604 | forEachBlock(function(block) { 605 | var element = blockRenderer(block) 606 | if (element) elements.push(element) 607 | }) 608 | return elements 609 | } 610 | return { isChar : isChar, 611 | toBlockX : toBlockX, 612 | toBlockY : toBlockY, 613 | blockCorner : blockCorner, 614 | blockCenter : blockCenter, 615 | forEachBlock : forEachBlock, 616 | sizeOf : sizeOf, 617 | randomBlock : randomBlock , 618 | render : render, 619 | renderWith : renderWith} 620 | } 621 | 622 | function getReadyData() { return [ 623 | " ____ _____ ______ ____ _____ __ ____ __ __ ", 624 | " ______ _____ ______ _____ _____ ____ _____ __ __ ", 625 | " __ __ __ __ __ __ __ __ __ __ __ __ ", 626 | " __ __ __ __ __ __ __ __ __ __ __ __ ", 627 | " __ __ _____ __ _____ _____ ______ __ __ ____ ", 628 | " __ __ _____ __ ____ _____ ______ __ __ __ ", 629 | " __ __ __ __ __ __ __ __ __ __ __ __ ", 630 | " __ __ __ __ __ __ __ __ __ __ __ __ ", 631 | " ______ _____ __ __ __ _____ __ __ _____ __ ", 632 | " ____ _____ __ __ __ _____ __ __ ____ __ " 633 | ]} 634 | function goData() { return [ 635 | " ____ ____ ", 636 | " ______ ______ ", 637 | " __ __ __ ", 638 | " __ __ __ ", 639 | " __ __ __ __ ", 640 | " __ __ __ __ ", 641 | " __ __ __ __ ", 642 | " __ __ __ __ ", 643 | " ______ ______ ", 644 | " ____ ____ " 645 | ]} 646 | function startScreenData() { return [ 647 | " __ __ __ ____ ____ ______ ____ __ __ ______", 648 | " __ __ __ ______ _____ ______ ______ __ __ ______", 649 | " __ __ __ __ __ __ __ __ __ __ ___ __ __ ", 650 | " __ __ __ __ __ __ __ __ __ __ ____ __ __ ", 651 | " __ __ __ __ __ _____ __ __ __ __ ____ ______", 652 | " __ __ __ __ __ ____ __ __ __ __ ___ ______", 653 | " __ __ __ __ __ __ __ __ __ __ __ __ __ ", 654 | " ________ __ __ __ __ __ __ __ __ __ __ ", 655 | " __ __ ______ __ __ ______ ______ __ __ ______", 656 | " __ __ ____ __ __ ______ ____ __ __ ______", 657 | " ", 658 | " ", 659 | " ", 660 | " ", 661 | " P R E S S A N Y K E Y ", 662 | ]} 663 | 664 | 665 | var delay = 50 666 | var left = Point(-1, 0), right = Point(1, 0), up = Point(0, -1), down = Point(0, 1) 667 | var imgPath = "images/" 668 | 669 | function randomInt(limit) { return Math.floor(Math.random() * limit) } 670 | function identity(x) { return x } 671 | function first(xs) { return xs ? xs[0] : undefined} 672 | function latter (_, second) { return second } 673 | function both (first, second) { return [first, second] } 674 | function extractProperty(property) { return function(x) { return x.property } } 675 | function toArray(x) { return !x ? [] : (_.isArray(x) ? x : [x])} 676 | var gameTicker = ticker(delay) 677 | function ticker(interval) { 678 | return Bacon.interval(interval) 679 | } 680 | function always(value) { return function(_) { return value } } 681 | function atMostOne(array) { return array.length <= 1 } 682 | function print(x) { console.log(x) } 683 | function toConsole(stream, prefix) { stream.onValue( function(item) { console.log(prefix + ":" + item) })} 684 | function Rectangle(x, y, width, height) { 685 | return {x : x, y : y, width : width, height : height} 686 | } 687 | function removeElements(elements) { 688 | return function() { toArray(elements).forEach(function(element){ 689 | element.remove()}) } 690 | } 691 | Bacon.EventStream.prototype.multiply = function(times) { 692 | return this.withHandler(function(event) { 693 | if (event.isEnd()) 694 | this.push(event) 695 | else 696 | for (var i = 0; i < times; i++) { 697 | var reply = this.push(event) 698 | if (reply == Bacon.noMore) return reply 699 | } 700 | return Bacon.more 701 | }) 702 | } 703 | -------------------------------------------------------------------------------- /lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.1.4 2 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | 9 | (function() { 10 | 11 | // Baseline setup 12 | // -------------- 13 | 14 | // Establish the root object, `window` in the browser, or `global` on the server. 15 | var root = this; 16 | 17 | // Save the previous value of the `_` variable. 18 | var previousUnderscore = root._; 19 | 20 | // Establish the object that gets returned to break out of a loop iteration. 21 | var breaker = {}; 22 | 23 | // Save bytes in the minified (but not gzipped) version: 24 | var ArrayProto = Array.prototype, ObjProto = Object.prototype; 25 | 26 | // Create quick reference variables for speed access to core prototypes. 27 | var slice = ArrayProto.slice, 28 | unshift = ArrayProto.unshift, 29 | toString = ObjProto.toString, 30 | hasOwnProperty = ObjProto.hasOwnProperty; 31 | 32 | // All **ECMAScript 5** native function implementations that we hope to use 33 | // are declared here. 34 | var 35 | nativeForEach = ArrayProto.forEach, 36 | nativeMap = ArrayProto.map, 37 | nativeReduce = ArrayProto.reduce, 38 | nativeReduceRight = ArrayProto.reduceRight, 39 | nativeFilter = ArrayProto.filter, 40 | nativeEvery = ArrayProto.every, 41 | nativeSome = ArrayProto.some, 42 | nativeIndexOf = ArrayProto.indexOf, 43 | nativeLastIndexOf = ArrayProto.lastIndexOf, 44 | nativeIsArray = Array.isArray, 45 | nativeKeys = Object.keys; 46 | 47 | // Create a safe reference to the Underscore object for use below. 48 | var _ = function(obj) { return new wrapper(obj); }; 49 | 50 | // Export the Underscore object for **CommonJS**, with backwards-compatibility 51 | // for the old `require()` API. If we're not in CommonJS, add `_` to the 52 | // global object. 53 | if (typeof module !== 'undefined' && module.exports) { 54 | module.exports = _; 55 | _._ = _; 56 | } else { 57 | root._ = _; 58 | } 59 | 60 | // Current version. 61 | _.VERSION = '1.1.4'; 62 | 63 | // Collection Functions 64 | // -------------------- 65 | 66 | // The cornerstone, an `each` implementation, aka `forEach`. 67 | // Handles objects implementing `forEach`, arrays, and raw objects. 68 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 69 | var each = _.each = _.forEach = function(obj, iterator, context) { 70 | var value; 71 | if (obj == null) return; 72 | if (nativeForEach && obj.forEach === nativeForEach) { 73 | obj.forEach(iterator, context); 74 | } else if (_.isNumber(obj.length)) { 75 | for (var i = 0, l = obj.length; i < l; i++) { 76 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 77 | } 78 | } else { 79 | for (var key in obj) { 80 | if (hasOwnProperty.call(obj, key)) { 81 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 82 | } 83 | } 84 | } 85 | }; 86 | 87 | // Return the results of applying the iterator to each element. 88 | // Delegates to **ECMAScript 5**'s native `map` if available. 89 | _.map = function(obj, iterator, context) { 90 | var results = []; 91 | if (obj == null) return results; 92 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 93 | each(obj, function(value, index, list) { 94 | results[results.length] = iterator.call(context, value, index, list); 95 | }); 96 | return results; 97 | }; 98 | 99 | // **Reduce** builds up a single result from a list of values, aka `inject`, 100 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 101 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 102 | var initial = memo !== void 0; 103 | if (obj == null) obj = []; 104 | if (nativeReduce && obj.reduce === nativeReduce) { 105 | if (context) iterator = _.bind(iterator, context); 106 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 107 | } 108 | each(obj, function(value, index, list) { 109 | if (!initial && index === 0) { 110 | memo = value; 111 | initial = true; 112 | } else { 113 | memo = iterator.call(context, memo, value, index, list); 114 | } 115 | }); 116 | if (!initial) throw new TypeError("Reduce of empty array with no initial value"); 117 | return memo; 118 | }; 119 | 120 | // The right-associative version of reduce, also known as `foldr`. 121 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 122 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 123 | if (obj == null) obj = []; 124 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 125 | if (context) iterator = _.bind(iterator, context); 126 | return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 127 | } 128 | var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse(); 129 | return _.reduce(reversed, iterator, memo, context); 130 | }; 131 | 132 | // Return the first value which passes a truth test. Aliased as `detect`. 133 | _.find = _.detect = function(obj, iterator, context) { 134 | var result; 135 | any(obj, function(value, index, list) { 136 | if (iterator.call(context, value, index, list)) { 137 | result = value; 138 | return true; 139 | } 140 | }); 141 | return result; 142 | }; 143 | 144 | // Return all the elements that pass a truth test. 145 | // Delegates to **ECMAScript 5**'s native `filter` if available. 146 | // Aliased as `select`. 147 | _.filter = _.select = function(obj, iterator, context) { 148 | var results = []; 149 | if (obj == null) return results; 150 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 151 | each(obj, function(value, index, list) { 152 | if (iterator.call(context, value, index, list)) results[results.length] = value; 153 | }); 154 | return results; 155 | }; 156 | 157 | // Return all the elements for which a truth test fails. 158 | _.reject = function(obj, iterator, context) { 159 | var results = []; 160 | if (obj == null) return results; 161 | each(obj, function(value, index, list) { 162 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 163 | }); 164 | return results; 165 | }; 166 | 167 | // Determine whether all of the elements match a truth test. 168 | // Delegates to **ECMAScript 5**'s native `every` if available. 169 | // Aliased as `all`. 170 | _.every = _.all = function(obj, iterator, context) { 171 | iterator = iterator || _.identity; 172 | var result = true; 173 | if (obj == null) return result; 174 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 175 | each(obj, function(value, index, list) { 176 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 177 | }); 178 | return result; 179 | }; 180 | 181 | // Determine if at least one element in the object matches a truth test. 182 | // Delegates to **ECMAScript 5**'s native `some` if available. 183 | // Aliased as `any`. 184 | var any = _.some = _.any = function(obj, iterator, context) { 185 | iterator = iterator || _.identity; 186 | var result = false; 187 | if (obj == null) return result; 188 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 189 | each(obj, function(value, index, list) { 190 | if (result = iterator.call(context, value, index, list)) return breaker; 191 | }); 192 | return result; 193 | }; 194 | 195 | // Determine if a given value is included in the array or object using `===`. 196 | // Aliased as `contains`. 197 | _.include = _.contains = function(obj, target) { 198 | var found = false; 199 | if (obj == null) return found; 200 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 201 | any(obj, function(value) { 202 | if (found = value === target) return true; 203 | }); 204 | return found; 205 | }; 206 | 207 | // Invoke a method (with arguments) on every item in a collection. 208 | _.invoke = function(obj, method) { 209 | var args = slice.call(arguments, 2); 210 | return _.map(obj, function(value) { 211 | return (method ? value[method] : value).apply(value, args); 212 | }); 213 | }; 214 | 215 | // Convenience version of a common use case of `map`: fetching a property. 216 | _.pluck = function(obj, key) { 217 | return _.map(obj, function(value){ return value[key]; }); 218 | }; 219 | 220 | // Return the maximum element or (element-based computation). 221 | _.max = function(obj, iterator, context) { 222 | if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); 223 | var result = {computed : -Infinity}; 224 | each(obj, function(value, index, list) { 225 | var computed = iterator ? iterator.call(context, value, index, list) : value; 226 | computed >= result.computed && (result = {value : value, computed : computed}); 227 | }); 228 | return result.value; 229 | }; 230 | 231 | // Return the minimum element (or element-based computation). 232 | _.min = function(obj, iterator, context) { 233 | if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); 234 | var result = {computed : Infinity}; 235 | each(obj, function(value, index, list) { 236 | var computed = iterator ? iterator.call(context, value, index, list) : value; 237 | computed < result.computed && (result = {value : value, computed : computed}); 238 | }); 239 | return result.value; 240 | }; 241 | 242 | // Sort the object's values by a criterion produced by an iterator. 243 | _.sortBy = function(obj, iterator, context) { 244 | return _.pluck(_.map(obj, function(value, index, list) { 245 | return { 246 | value : value, 247 | criteria : iterator.call(context, value, index, list) 248 | }; 249 | }).sort(function(left, right) { 250 | var a = left.criteria, b = right.criteria; 251 | return a < b ? -1 : a > b ? 1 : 0; 252 | }), 'value'); 253 | }; 254 | 255 | // Use a comparator function to figure out at what index an object should 256 | // be inserted so as to maintain order. Uses binary search. 257 | _.sortedIndex = function(array, obj, iterator) { 258 | iterator = iterator || _.identity; 259 | var low = 0, high = array.length; 260 | while (low < high) { 261 | var mid = (low + high) >> 1; 262 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 263 | } 264 | return low; 265 | }; 266 | 267 | // Safely convert anything iterable into a real, live array. 268 | _.toArray = function(iterable) { 269 | if (!iterable) return []; 270 | if (iterable.toArray) return iterable.toArray(); 271 | if (_.isArray(iterable)) return iterable; 272 | if (_.isArguments(iterable)) return slice.call(iterable); 273 | return _.values(iterable); 274 | }; 275 | 276 | // Return the number of elements in an object. 277 | _.size = function(obj) { 278 | return _.toArray(obj).length; 279 | }; 280 | 281 | // Array Functions 282 | // --------------- 283 | 284 | // Get the first element of an array. Passing **n** will return the first N 285 | // values in the array. Aliased as `head`. The **guard** check allows it to work 286 | // with `_.map`. 287 | _.first = _.head = function(array, n, guard) { 288 | return n && !guard ? slice.call(array, 0, n) : array[0]; 289 | }; 290 | 291 | // Returns everything but the first entry of the array. Aliased as `tail`. 292 | // Especially useful on the arguments object. Passing an **index** will return 293 | // the rest of the values in the array from that index onward. The **guard** 294 | // check allows it to work with `_.map`. 295 | _.rest = _.tail = function(array, index, guard) { 296 | return slice.call(array, _.isUndefined(index) || guard ? 1 : index); 297 | }; 298 | 299 | // Get the last element of an array. 300 | _.last = function(array) { 301 | return array[array.length - 1]; 302 | }; 303 | 304 | // Trim out all falsy values from an array. 305 | _.compact = function(array) { 306 | return _.filter(array, function(value){ return !!value; }); 307 | }; 308 | 309 | // Return a completely flattened version of an array. 310 | _.flatten = function(array) { 311 | return _.reduce(array, function(memo, value) { 312 | if (_.isArray(value)) return memo.concat(_.flatten(value)); 313 | memo[memo.length] = value; 314 | return memo; 315 | }, []); 316 | }; 317 | 318 | // Return a version of the array that does not contain the specified value(s). 319 | _.without = function(array) { 320 | var values = slice.call(arguments, 1); 321 | return _.filter(array, function(value){ return !_.include(values, value); }); 322 | }; 323 | 324 | // Produce a duplicate-free version of the array. If the array has already 325 | // been sorted, you have the option of using a faster algorithm. 326 | // Aliased as `unique`. 327 | _.uniq = _.unique = function(array, isSorted) { 328 | return _.reduce(array, function(memo, el, i) { 329 | if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el; 330 | return memo; 331 | }, []); 332 | }; 333 | 334 | // Produce an array that contains every item shared between all the 335 | // passed-in arrays. 336 | _.intersect = function(array) { 337 | var rest = slice.call(arguments, 1); 338 | return _.filter(_.uniq(array), function(item) { 339 | return _.every(rest, function(other) { 340 | return _.indexOf(other, item) >= 0; 341 | }); 342 | }); 343 | }; 344 | 345 | // Zip together multiple lists into a single array -- elements that share 346 | // an index go together. 347 | _.zip = function() { 348 | var args = slice.call(arguments); 349 | var length = _.max(_.pluck(args, 'length')); 350 | var results = new Array(length); 351 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 352 | return results; 353 | }; 354 | 355 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 356 | // we need this function. Return the position of the first occurrence of an 357 | // item in an array, or -1 if the item is not included in the array. 358 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 359 | // If the array is large and already in sort order, pass `true` 360 | // for **isSorted** to use binary search. 361 | _.indexOf = function(array, item, isSorted) { 362 | if (array == null) return -1; 363 | if (isSorted) { 364 | var i = _.sortedIndex(array, item); 365 | return array[i] === item ? i : -1; 366 | } 367 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 368 | for (var i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; 369 | return -1; 370 | }; 371 | 372 | 373 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 374 | _.lastIndexOf = function(array, item) { 375 | if (array == null) return -1; 376 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 377 | var i = array.length; 378 | while (i--) if (array[i] === item) return i; 379 | return -1; 380 | }; 381 | 382 | // Generate an integer Array containing an arithmetic progression. A port of 383 | // the native Python `range()` function. See 384 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 385 | _.range = function(start, stop, step) { 386 | var args = slice.call(arguments), 387 | solo = args.length <= 1, 388 | start = solo ? 0 : args[0], 389 | stop = solo ? args[0] : args[1], 390 | step = args[2] || 1, 391 | len = Math.max(Math.ceil((stop - start) / step), 0), 392 | idx = 0, 393 | range = new Array(len); 394 | while (idx < len) { 395 | range[idx++] = start; 396 | start += step; 397 | } 398 | return range; 399 | }; 400 | 401 | // Function (ahem) Functions 402 | // ------------------ 403 | 404 | // Create a function bound to a given object (assigning `this`, and arguments, 405 | // optionally). Binding with arguments is also known as `curry`. 406 | _.bind = function(func, obj) { 407 | var args = slice.call(arguments, 2); 408 | return function() { 409 | return func.apply(obj || {}, args.concat(slice.call(arguments))); 410 | }; 411 | }; 412 | 413 | // Bind all of an object's methods to that object. Useful for ensuring that 414 | // all callbacks defined on an object belong to it. 415 | _.bindAll = function(obj) { 416 | var funcs = slice.call(arguments, 1); 417 | if (funcs.length == 0) funcs = _.functions(obj); 418 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 419 | return obj; 420 | }; 421 | 422 | // Memoize an expensive function by storing its results. 423 | _.memoize = function(func, hasher) { 424 | var memo = {}; 425 | hasher = hasher || _.identity; 426 | return function() { 427 | var key = hasher.apply(this, arguments); 428 | return key in memo ? memo[key] : (memo[key] = func.apply(this, arguments)); 429 | }; 430 | }; 431 | 432 | // Delays a function for the given number of milliseconds, and then calls 433 | // it with the arguments supplied. 434 | _.delay = function(func, wait) { 435 | var args = slice.call(arguments, 2); 436 | return setTimeout(function(){ return func.apply(func, args); }, wait); 437 | }; 438 | 439 | // Defers a function, scheduling it to run after the current call stack has 440 | // cleared. 441 | _.defer = function(func) { 442 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 443 | }; 444 | 445 | // Internal function used to implement `_.throttle` and `_.debounce`. 446 | var limit = function(func, wait, debounce) { 447 | var timeout; 448 | return function() { 449 | var context = this, args = arguments; 450 | var throttler = function() { 451 | timeout = null; 452 | func.apply(context, args); 453 | }; 454 | if (debounce) clearTimeout(timeout); 455 | if (debounce || !timeout) timeout = setTimeout(throttler, wait); 456 | }; 457 | }; 458 | 459 | // Returns a function, that, when invoked, will only be triggered at most once 460 | // during a given window of time. 461 | _.throttle = function(func, wait) { 462 | return limit(func, wait, false); 463 | }; 464 | 465 | // Returns a function, that, as long as it continues to be invoked, will not 466 | // be triggered. The function will be called after it stops being called for 467 | // N milliseconds. 468 | _.debounce = function(func, wait) { 469 | return limit(func, wait, true); 470 | }; 471 | 472 | // Returns the first function passed as an argument to the second, 473 | // allowing you to adjust arguments, run code before and after, and 474 | // conditionally execute the original function. 475 | _.wrap = function(func, wrapper) { 476 | return function() { 477 | var args = [func].concat(slice.call(arguments)); 478 | return wrapper.apply(this, args); 479 | }; 480 | }; 481 | 482 | // Returns a function that is the composition of a list of functions, each 483 | // consuming the return value of the function that follows. 484 | _.compose = function() { 485 | var funcs = slice.call(arguments); 486 | return function() { 487 | var args = slice.call(arguments); 488 | for (var i=funcs.length-1; i >= 0; i--) { 489 | args = [funcs[i].apply(this, args)]; 490 | } 491 | return args[0]; 492 | }; 493 | }; 494 | 495 | // Object Functions 496 | // ---------------- 497 | 498 | // Retrieve the names of an object's properties. 499 | // Delegates to **ECMAScript 5**'s native `Object.keys` 500 | _.keys = nativeKeys || function(obj) { 501 | if (_.isArray(obj)) return _.range(0, obj.length); 502 | var keys = []; 503 | for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key; 504 | return keys; 505 | }; 506 | 507 | // Retrieve the values of an object's properties. 508 | _.values = function(obj) { 509 | return _.map(obj, _.identity); 510 | }; 511 | 512 | // Return a sorted list of the function names available on the object. 513 | // Aliased as `methods` 514 | _.functions = _.methods = function(obj) { 515 | return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort(); 516 | }; 517 | 518 | // Extend a given object with all the properties in passed-in object(s). 519 | _.extend = function(obj) { 520 | each(slice.call(arguments, 1), function(source) { 521 | for (var prop in source) obj[prop] = source[prop]; 522 | }); 523 | return obj; 524 | }; 525 | 526 | // Create a (shallow-cloned) duplicate of an object. 527 | _.clone = function(obj) { 528 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 529 | }; 530 | 531 | // Invokes interceptor with the obj, and then returns obj. 532 | // The primary purpose of this method is to "tap into" a method chain, in 533 | // order to perform operations on intermediate results within the chain. 534 | _.tap = function(obj, interceptor) { 535 | interceptor(obj); 536 | return obj; 537 | }; 538 | 539 | // Perform a deep comparison to check if two objects are equal. 540 | _.isEqual = function(a, b) { 541 | // Check object identity. 542 | if (a === b) return true; 543 | // Different types? 544 | var atype = typeof(a), btype = typeof(b); 545 | if (atype != btype) return false; 546 | // Basic equality test (watch out for coercions). 547 | if (a == b) return true; 548 | // One is falsy and the other truthy. 549 | if ((!a && b) || (a && !b)) return false; 550 | // Unwrap any wrapped objects. 551 | if (a._chain) a = a._wrapped; 552 | if (b._chain) b = b._wrapped; 553 | // One of them implements an isEqual()? 554 | if (a.isEqual) return a.isEqual(b); 555 | // Check dates' integer values. 556 | if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime(); 557 | // Both are NaN? 558 | if (_.isNaN(a) && _.isNaN(b)) return false; 559 | // Compare regular expressions. 560 | if (_.isRegExp(a) && _.isRegExp(b)) 561 | return a.source === b.source && 562 | a.global === b.global && 563 | a.ignoreCase === b.ignoreCase && 564 | a.multiline === b.multiline; 565 | // If a is not an object by this point, we can't handle it. 566 | if (atype !== 'object') return false; 567 | // Check for different array lengths before comparing contents. 568 | if (a.length && (a.length !== b.length)) return false; 569 | // Nothing else worked, deep compare the contents. 570 | var aKeys = _.keys(a), bKeys = _.keys(b); 571 | // Different object sizes? 572 | if (aKeys.length != bKeys.length) return false; 573 | // Recursive comparison of contents. 574 | for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; 575 | return true; 576 | }; 577 | 578 | // Is a given array or object empty? 579 | _.isEmpty = function(obj) { 580 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 581 | for (var key in obj) if (hasOwnProperty.call(obj, key)) return false; 582 | return true; 583 | }; 584 | 585 | // Is a given value a DOM element? 586 | _.isElement = function(obj) { 587 | return !!(obj && obj.nodeType == 1); 588 | }; 589 | 590 | // Is a given value an array? 591 | // Delegates to ECMA5's native Array.isArray 592 | _.isArray = nativeIsArray || function(obj) { 593 | return toString.call(obj) === '[object Array]'; 594 | }; 595 | 596 | // Is a given variable an arguments object? 597 | _.isArguments = function(obj) { 598 | return !!(obj && hasOwnProperty.call(obj, 'callee')); 599 | }; 600 | 601 | // Is a given value a function? 602 | _.isFunction = function(obj) { 603 | return !!(obj && obj.constructor && obj.call && obj.apply); 604 | }; 605 | 606 | // Is a given value a string? 607 | _.isString = function(obj) { 608 | return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); 609 | }; 610 | 611 | // Is a given value a number? 612 | _.isNumber = function(obj) { 613 | return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed)); 614 | }; 615 | 616 | // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript 617 | // that does not equal itself. 618 | _.isNaN = function(obj) { 619 | return obj !== obj; 620 | }; 621 | 622 | // Is a given value a boolean? 623 | _.isBoolean = function(obj) { 624 | return obj === true || obj === false; 625 | }; 626 | 627 | // Is a given value a date? 628 | _.isDate = function(obj) { 629 | return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear); 630 | }; 631 | 632 | // Is the given value a regular expression? 633 | _.isRegExp = function(obj) { 634 | return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false)); 635 | }; 636 | 637 | // Is a given value equal to null? 638 | _.isNull = function(obj) { 639 | return obj === null; 640 | }; 641 | 642 | // Is a given variable undefined? 643 | _.isUndefined = function(obj) { 644 | return obj === void 0; 645 | }; 646 | 647 | // Utility Functions 648 | // ----------------- 649 | 650 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 651 | // previous owner. Returns a reference to the Underscore object. 652 | _.noConflict = function() { 653 | root._ = previousUnderscore; 654 | return this; 655 | }; 656 | 657 | // Keep the identity function around for default iterators. 658 | _.identity = function(value) { 659 | return value; 660 | }; 661 | 662 | // Run a function **n** times. 663 | _.times = function (n, iterator, context) { 664 | for (var i = 0; i < n; i++) iterator.call(context, i); 665 | }; 666 | 667 | // Add your own custom functions to the Underscore object, ensuring that 668 | // they're correctly added to the OOP wrapper as well. 669 | _.mixin = function(obj) { 670 | each(_.functions(obj), function(name){ 671 | addToWrapper(name, _[name] = obj[name]); 672 | }); 673 | }; 674 | 675 | // Generate a unique integer id (unique within the entire client session). 676 | // Useful for temporary DOM ids. 677 | var idCounter = 0; 678 | _.uniqueId = function(prefix) { 679 | var id = idCounter++; 680 | return prefix ? prefix + id : id; 681 | }; 682 | 683 | // By default, Underscore uses ERB-style template delimiters, change the 684 | // following template settings to use alternative delimiters. 685 | _.templateSettings = { 686 | evaluate : /<%([\s\S]+?)%>/g, 687 | interpolate : /<%=([\s\S]+?)%>/g 688 | }; 689 | 690 | // JavaScript micro-templating, similar to John Resig's implementation. 691 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 692 | // and correctly escapes quotes within interpolated code. 693 | _.template = function(str, data) { 694 | var c = _.templateSettings; 695 | var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + 696 | 'with(obj||{}){__p.push(\'' + 697 | str.replace(/\\/g, '\\\\') 698 | .replace(/'/g, "\\'") 699 | .replace(c.interpolate, function(match, code) { 700 | return "'," + code.replace(/\\'/g, "'") + ",'"; 701 | }) 702 | .replace(c.evaluate || null, function(match, code) { 703 | return "');" + code.replace(/\\'/g, "'") 704 | .replace(/[\r\n\t]/g, ' ') + "__p.push('"; 705 | }) 706 | .replace(/\r/g, '\\r') 707 | .replace(/\n/g, '\\n') 708 | .replace(/\t/g, '\\t') 709 | + "');}return __p.join('');"; 710 | var func = new Function('obj', tmpl); 711 | return data ? func(data) : func; 712 | }; 713 | 714 | // The OOP Wrapper 715 | // --------------- 716 | 717 | // If Underscore is called as a function, it returns a wrapped object that 718 | // can be used OO-style. This wrapper holds altered versions of all the 719 | // underscore functions. Wrapped objects may be chained. 720 | var wrapper = function(obj) { this._wrapped = obj; }; 721 | 722 | // Expose `wrapper.prototype` as `_.prototype` 723 | _.prototype = wrapper.prototype; 724 | 725 | // Helper function to continue chaining intermediate results. 726 | var result = function(obj, chain) { 727 | return chain ? _(obj).chain() : obj; 728 | }; 729 | 730 | // A method to easily add functions to the OOP wrapper. 731 | var addToWrapper = function(name, func) { 732 | wrapper.prototype[name] = function() { 733 | var args = slice.call(arguments); 734 | unshift.call(args, this._wrapped); 735 | return result(func.apply(_, args), this._chain); 736 | }; 737 | }; 738 | 739 | // Add all of the Underscore functions to the wrapper object. 740 | _.mixin(_); 741 | 742 | // Add all mutator Array functions to the wrapper. 743 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 744 | var method = ArrayProto[name]; 745 | wrapper.prototype[name] = function() { 746 | method.apply(this._wrapped, arguments); 747 | return result(this._wrapped, this._chain); 748 | }; 749 | }); 750 | 751 | // Add all accessor Array functions to the wrapper. 752 | each(['concat', 'join', 'slice'], function(name) { 753 | var method = ArrayProto[name]; 754 | wrapper.prototype[name] = function() { 755 | return result(method.apply(this._wrapped, arguments), this._chain); 756 | }; 757 | }); 758 | 759 | // Start chaining a wrapped Underscore object. 760 | wrapper.prototype.chain = function() { 761 | this._chain = true; 762 | return this; 763 | }; 764 | 765 | // Extracts the result from a wrapped and chained object. 766 | wrapper.prototype.value = function() { 767 | return this._wrapped; 768 | }; 769 | 770 | })(); 771 | --------------------------------------------------------------------------------