├── .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 |
--------------------------------------------------------------------------------