JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
hold blip effects in a data structure
[hexbog.git] / main.coffee
1 #   HexBog, a word game
2 #   Copyright (C) 2012 Jason Woofenden
3
4 #   This program is free software: you can redistribute it and/or modify
5 #   it under the terms of the GNU Affero General Public License as published by
6 #   the Free Software Foundation, either version 3 of the License, or
7 #   (at your option) any later version.
8
9 #   This program is distributed in the hope that it will be useful,
10 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #   GNU Affero General Public License for more details.
13
14 #   You should have received a copy of the GNU Affero General Public License
15 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 ##############################################
18 ##############    settings    ################
19 ##############################################
20
21 tile_radius = 26
22 tile_width = tile_radius * 2
23
24 fade_ms = 400
25 slide_ms = 2000
26
27 columns = [
28         { height: 5, spaces: [], fader_count: 0 }
29         { height: 6, spaces: [], fader_count: 0 }
30         { height: 7, spaces: [], fader_count: 0 }
31         { height: 8, spaces: [], fader_count: 0 }
32         { height: 7, spaces: [], fader_count: 0 }
33         { height: 6, spaces: [], fader_count: 0 }
34         { height: 5, spaces: [], fader_count: 0 }
35 ]
36
37 # code and css will need adjusting if you change HP_MAX
38 HP_MAX = 10
39
40 ##############################################################
41 ##############    fix javascript some more    ################
42 ##############################################################
43
44 # so annoying that setTimeout has its arguments in the wrong order
45 timeout = (ms, callback) ->
46         setTimeout callback, ms
47
48 # warning: it's shalow (sub-elements are not cloned)
49 Array::clone = ->
50         return this.slice(0)
51
52 Array::sum = ->
53         ret = 0
54         ret += i for i in this
55         return ret
56
57 Array::last = ->
58         return this[this.length - 1]
59
60
61 ##############################################################
62 ##############    cookies (auto-save game)    ################
63 ##############################################################
64
65 set_cookie = (name, value, days) ->
66         date = new Date()
67         date.setTime date.getTime()+(days*24*60*60*1000)
68         cookie = "#{name}=#{value}; expires=#{date.toGMTString()}; path=/"
69         document.cookie = cookie
70 window.sc = set_cookie
71
72 get_cookie = (name) ->
73         key = name + '='
74         for c in document.cookie.split /; */
75                 if c.indexOf key is 0
76                         return c.substr key.length
77         return null
78
79 delete_cookie = (name) ->
80         set_cookie name, '', -1
81 window.dc = delete_cookie
82
83
84 num_spaces = 0
85 num_spaces += column.height for column in columns
86 score = 0
87 spaces = new Array(num_spaces)
88
89 selected = []
90
91 letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
92 letter_distribution = [
93         14355 # a
94          3968 # b
95          6325 # c
96          7045 # d
97         20258 # e
98          2739 # f
99          5047 # g
100          4372 # h
101         13053 # i
102           516 # j
103          2600 # k
104          9631 # l
105          5115 # m
106         10082 # n
107         11142 # o
108          5292 # p
109           287 # qu
110         12341 # r
111         16571 # s
112         10215 # t
113          6131 # u
114          1728 # v
115          2184 # w
116           619 # x
117          3512 # y
118           831 # z
119
120 ]
121
122 letter_distribution_total = 175973 # letter_distribution.sum()
123
124
125 new_letter_queue = []
126 new_letter = ->
127         if new_letter_queue.length
128                 l = new_letter_queue.shift()
129                 l.letter = l.letter.toUpperCase()
130                 if l.letter is 'Q'
131                         l.letter = 'Qu'
132                 return l
133         hp = 1 + Math.floor(Math.random() * (HP_MAX - 1))
134         r = Math.floor Math.random() * (letter_distribution_total + 1)
135         for i in [0..25]
136                 r -= letter_distribution[i]
137                 if r <= 0
138                         if letters[i] is 'Q'
139                                 return letter: 'Qu', hp: hp
140                         return letter: letters[i], hp: hp
141         return letter: 'Z', hp: hp # just in case
142
143
144
145 # in memory it's layed out like this:
146 # a c f j m
147 # b d g k n
148 #   e h l
149 #     i
150 # for display, columns are slid vertically like so:
151 #       f
152 #     c   j
153 #   a   g   m
154 #     d   k
155 #   b   h   n
156 #     e   l
157 #       i
158 #
159 # work out which grid spaces are connected
160 init_board_layout = () ->
161         col_offset = 0
162         middle_col_num = (columns.length - 1) / 2
163
164         space_num = 0
165
166         for column, col_num in columns
167                 if col_num < middle_col_num
168                         fw_other = 1
169                 else
170                         fw_other = -1
171
172                 if col_num > middle_col_num
173                         bw_other = 1
174                 else
175                         bw_other = -1
176
177                 is_first_col = col_num is 0
178                 is_last_col = col_num is columns.length - 1
179
180                 neighbors = []
181                 # neighbors are integers for now, but get dereferenced later, after we've created all the spaces
182                 push = (offset) ->
183                         neighbors.push col_offset + offset
184
185                 col_top_px = Math.abs col_num - middle_col_num
186                 col_top_px *= tile_radius
187
188                 above = []
189                 for i in [0 ... column.height]
190                         space = { id: space_num }
191                         spaces[space_num] = space
192                         space_num += 1
193                         column.spaces.push space
194                         space.column = column
195
196                         is_top_tile = i is 0
197                         is_bottom_tile = i is column.height - 1
198
199                         # link tile number to pixel "top" and "left" of containing column
200                         space.top_px = col_top_px + i * tile_width
201                         space.left_px = col_num * tile_width
202
203                         # aboves: array of spaces, top to bottom
204                         space.aboves = above.clone()
205                         above.push space
206
207                         # below: SINGLE tile below
208                         unless is_top_tile
209                                 spaces[space.id - 1].below = space
210
211                         # neighbors (array of tile numbers "next to" this one)
212                         neighbors = []
213                         unless is_top_tile # upward link
214                                 push i - 1
215                         unless is_bottom_tile # downward links
216                                 push i + 1
217                         unless is_first_col # leftward links
218                                 unless is_bottom_tile and bw_other is -1
219                                         push i - columns[col_num - 1].height
220                                 unless is_top_tile and bw_other is -1
221                                         push i - columns[col_num - 1].height + bw_other
222                         unless is_last_col # rightward links
223                                 unless is_bottom_tile and fw_other is -1
224                                         push i + columns[col_num].height
225                                 unless is_top_tile and fw_other is -1
226                                         push i + columns[col_num].height + fw_other
227                         # will be dereferenced later
228                         space.neighbors = neighbors.clone() # FIXME ?remove ``.clone()``
229                 col_offset += column.height
230         # convert all space.neighbors arrays from containing space ids to referencing the space
231         for s in spaces
232                 for id, key in s.neighbors
233                         s.neighbors[key] = spaces[id]
234
235 # support obsolete save data format
236 load_game_0 = (encoded) ->
237         letters = (encoded.substr 0, num_spaces).split ''
238         for l in letters
239                 new_letter_queue.push {
240                         letter: l,
241                         hp: 1 + Math.floor(Math.random() * (HP_MAX - 1))
242                 }
243         score = parseInt(encoded.substr(num_spaces), 10)
244
245 load_game_1 = (encoded) ->
246         int = 0
247         encoded = encoded.substr 1
248         score = parseInt(encoded.substr(num_spaces * 3 / 2), 10)
249         for t in [0...(spaces.length * 3 / 2)] by 3
250                 int = 0
251                 for d in [0...3]
252                         int *= 44
253                         char = encoded[t + 2 - d]
254                         int += save_charset.indexOf(char)
255                 t2hp = int % 11
256                 int = Math.floor(int / 11)
257                 t2letter = String.fromCharCode(char_a + (int % 26))
258                 int = Math.floor(int / 26)
259                 t1hp = int % 11
260                 int = Math.floor(int / 11)
261                 t1letter = String.fromCharCode(char_a + (int % 26))
262                 new_letter_queue.push {
263                         letter: t1letter,
264                         hp: t1hp
265                 }
266                 new_letter_queue.push {
267                         letter: t2letter,
268                         hp: t2hp
269                 }
270
271 load_game = (encoded) ->
272         switch encoded.substr 0, 1
273                 when "1"
274                         load_game_1(encoded)
275                 else
276                         load_game_0(encoded)
277
278 init_board = ->
279         encoded = window.location.hash
280         if encoded? and encoded.charAt 0 is '#'
281                 encoded = encoded.substr 1
282         unless encoded? and encoded.length > num_spaces
283                 encoded = get_cookie 'hexbog'
284         if encoded? and encoded.length > num_spaces
285                 load_game encoded
286
287         # work out which grid spaces are connected
288         # (neighbors, above, down)
289         init_board_layout()
290
291 $big_tip = null # initialized by init_html_board
292 $little_tip = null # initialized by init_html_board
293 $score_display = null # initialized by init_html_board
294 $definition_body = null # initialized by init_html_board
295 update_selection_display = ->
296         word = selected_word()
297         $big_tip.removeClass('good')
298         if word.length > 0
299                 if word.length < 3
300                         $big_tip.html word
301                         $little_tip.html "Click more tiles (3 minimum)"
302                 else
303                         if is_word word
304                                 if word.indexOf(word.substr(word.length - 1)) < word.length - 1
305                                         last = 'last '
306                                 else
307                                         last = ''
308                                 $little_tip.html "Click the #{last}\"#{word.substr(word.length - 1)}\" for #{score_for word} points"
309                                 $big_tip.html "<a href=\"http://en.wiktionary.org/wiki/#{word}\" target=\"_blank\" title=\"click for definition\">#{word}</a>"
310                                 $big_tip.addClass('good')
311                         else
312                                 $big_tip.html word
313                                 $little_tip.html "\"#{word}\" is not in the word list."
314         else
315                 $big_tip.html "← Click a word"
316                 $little_tip.html "(tiles must be touching)"
317
318         # color the selected tiles according to whether they're a word or not
319         if word.length
320                 classes = ['selected_word', 'selected']
321                 if is_word word
322                         c = 0
323                 else
324                         c = 1
325                 for tile in selected
326                         tile.dom.addClass classes[c]
327                         tile.dom.removeClass classes[1 - c]
328
329 # unselects the last tile of the selecetion
330 unselect_tile = ->
331         _unselect_tile()
332         update_selection_display()
333
334 _unselect_tile = ->
335         html_tile = selected.pop().dom
336         html_tile.removeClass 'selected_word'
337         html_tile.removeClass 'selected'
338
339 unselect_all = ->
340         while selected.length
341                 _unselect_tile()
342         update_selection_display()
343
344 selected_word = ->
345         word = ''
346         word += tile.text for tile in selected
347         return word.toLowerCase()
348
349 save_charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR'
350 char_a = "a".charCodeAt(0)
351 save_game = ->
352         encoded = '1' # save format
353         for i in [0...spaces.length] by 2
354                 int = spaces[i].tile.text.toLowerCase().charCodeAt(0) - char_a
355                 int *= 11
356                 int += spaces[i].tile.hp
357                 int *= 26
358                 int += spaces[i+1].tile.text.toLowerCase().charCodeAt(0) - char_a
359                 int *= 11
360                 int += spaces[i+1].tile.hp
361                 for d in [0...3]
362                         encoded += save_charset.substr(int % 44, 1)
363                         int = Math.floor(int / 44)
364         encoded += score
365         set_cookie 'hexbog', encoded, 365
366         window.location.hash = encoded
367
368 unsink = (tile) ->
369         tile.new_hp = 10
370         tile.text = new_letter().letter
371         tile.dom.html tile.text
372
373
374 default_booms = {
375         3: {
376                 neighbors: {
377                         flips: 1
378                         force: 2
379                 }
380                 neighbor_neighbors: {
381                         flips: 0
382                         force: 0
383                 }
384                 board: {
385                         flips: 0
386                         force: 0
387                 }
388         }
389         4: {
390                 neighbors: {
391                         flips: 'all'
392                         force: 4
393                 }
394                 neighbor_neighbors: {
395                         flips: 0
396                         force: 2
397                 }
398                 board: {
399                         flips: 0
400                         force: 0
401                 }
402         }
403         5: {
404                 neighbors: {
405                         flips: 'all'
406                         force: 6
407                 }
408                 neighbor_neighbors: {
409                         flips: 2
410                         force: 4
411                 }
412                 board: {
413                         flips: 0
414                         force: 0
415                 }
416         }
417         6: {
418                 neighbors: {
419                         flips: 'all'
420                         force: 10
421                 }
422                 neighbor_neighbors: {
423                         flips: 5
424                         force: 6
425                 }
426                 board: {
427                         flips: 0
428                         force: 0
429                 }
430         }
431         7: {
432                 neighbors: {
433                         flips: 'all'
434                         force: 10
435                 }
436                 neighbor_neighbors: {
437                         flips: 'all'
438                         force: 10
439                 }
440                 board: {
441                         flips: 0
442                         force: 2
443                 }
444         }
445         lots: {
446                 neighbors: {
447                         flips: 0
448                         force: 0
449                 }
450                 neighbor_neighbors: {
451                         flips: 0
452                         force: 0
453                 }
454                 board: {
455                         flips: 'all'
456                         force: 10
457                 }
458         }
459 }
460 booms = null
461 init_booms = ->
462         booms = {}
463         for  level, areas of default_booms
464                 new_level = {}
465                 for area, effects of areas
466                         new_area = {}
467                         for effect, amount of effects
468                                 new_area[effect] = amount
469                         new_level[area] = new_area
470                 booms[level] = new_level
471         return
472
473 # remove the selected tiles from the board, create new tiles, and slide everything into place
474 blip_selection = ->
475         word_length = selected_word().length
476         faders = selected
477         selected = []
478         update_selection_display()
479         neighbors = {}
480         nneighbors = {}
481         for tile in faders
482                 tile.dom.unbind('click').fadeOut fade_ms
483                 tile.new_hp = tile.hp
484                 for n in tile.space.neighbors
485                         neighbors[n.id] = n.tile
486                         for nn in n.neighbors
487                                 nneighbors[nn.id] = nn.tile
488         # fix overlaps of faders, neighors, nneighbors
489         for tile in faders
490                 delete nneighbors[tile.space.id]
491                 delete neighbors[tile.space.id]
492         for k, v of neighbors
493                 delete nneighbors[k]
494         # convert to arrays so we can sort, etc
495         nneighbors = (v for k, v of nneighbors)
496         neighbors = (v for k, v of neighbors)
497         areas = {
498                 neighbors: {
499                         tiles: neighbors
500                         up: []
501                         down: []
502                 }
503                 neighbor_neighbors: {
504                         tiles: nneighbors
505                         up: []
506                         down: []
507                 }
508                 board: {
509                         tiles: (space.tile for space in spaces)
510                         up: []
511                         down: []
512                 }
513         }
514         for k, v of areas
515                 for t in v.tiles
516                         if t.hp is 0
517                                 v.down.push t
518                         else
519                                 v.up.push t
520         if word_length < 8
521                 boom = booms[word_length]
522         else
523                 boom = booms.lots
524         for area_name, effects of boom
525                 area = areas[area_name]
526                 if effects.flips is 'all' or effects.flips >= area.down.length
527                         for t in area.down
528                                 unsink t
529                 else
530                         down_count = area.down.length
531                         flips_left = effects.flips
532                         while flips_left > 0 and down_count > 0
533                                 flips_left -= 1
534                                 flipper = Math.floor(Math.random() * down_count)
535                                 unsink area.down[flipper]
536                                 down_count -= 1
537                                 # move the last tile back into range
538                                 area.down[flipper] = area.down[down_count]
539                 if effects.force > 0
540                         for tile in area.up
541                                 tile.new_hp = tile.hp + effects.force
542         for s in spaces
543                 s.tile.new_hp ?= s.tile.hp - 1
544                 if s.tile.new_hp < 0
545                         s.tile.new_hp = 0
546                 else if s.tile.new_hp > HP_MAX
547                         s.tile.new_hp = HP_MAX
548                 if s.tile.new_hp isnt s.tile.hp
549                         s.tile.dom.removeClass "hp#{s.tile.hp}"
550                         s.tile.dom.addClass "hp#{s.tile.new_hp}"
551                         s.tile.hp = s.tile.new_hp
552                 delete s.tile.new_hp
553         timeout fade_ms + 1, ->
554                 # delete old tiles, mark where tiles are moving
555                 for fader in faders
556                         fader.space.column.fader_count += 1
557                         fader.dom.remove()
558                         fader.removed = true
559                         for above in fader.space.aboves
560                                 if above.tile.dest?
561                                         above.tile.dest += 1
562                                 else
563                                         above.tile.dest = above.id + 1
564
565                 # move tiles down (graphically and in data structure)
566                 rspaces = []
567                 for s in spaces
568                         rspaces.unshift s
569                 for space in rspaces
570                         tile = space.tile
571                         if tile.dest? and not (tile.removed?)
572                                 dest_space = spaces[tile.dest]
573                                 delete tile.dest
574                                 tile.dom.animate {top: "#{dest_space.top_px}px"}, slide_ms
575                                 tile.space = dest_space
576                                 dest_space.tile = tile
577
578                 # create new tiles
579                 for column in columns
580                         dest = 0
581                         while column.fader_count > 0
582                                 column.fader_count -= 1
583                                 slide_from = -10 - tile_width
584                                 slide_from -= (50 + tile_width) * column.fader_count
585                                 space = column.spaces[dest++]
586                                 tile = new_tile space, slide_from
587                                 tile.dom.animate {top: "#{space.top_px}px"}, slide_ms
588
589                 save_game()
590
591 score_for = (word) -> Math.round(Math.pow(1.7, word.length))
592
593 activate_selection = ->
594         word = selected_word()
595         if word.length < 3
596                 # FIXME make this a hint
597                 log "Too short: \"#{word}\""
598                 return
599         unless is_word word
600                 # FIXME make this automatically part of the selection display
601                 log "Not on word list: \"#{word}\""
602                 return
603         word_score = score_for word
604         score += word_score
605         $score_display.html score
606         # FIXME make some kind of animation showing score gain
607         blip_selection()
608         look_up_definition word
609         $('#definition').click()
610
611
612 show_definition = (word, type, definition, language) ->
613         html = "<a href=\"http://en.wiktionary.org/wiki/#{word}\" target=\"_blank\">"
614         html += "#{word.substr(0, 1).toUpperCase() + word.substr(1)}</a>, #{type}"
615         if language isnt 'English'
616                 html += " (#{language})"
617         html += ': '
618         html += definition
619         html += '<div id="definition_credit">Definition &copy;<a href="http://en.wiktionary.org/" target="_blank">wiktionary.org</a> CC-BY-SA</div>'
620         $definition_body.html html
621
622
623 select_tile = (tile) ->
624         selected.push tile
625         update_selection_display()
626
627 new_tile = (space, y) ->
628         x = space.left_px
629         l = new_letter()
630         letter = l.letter
631         hp = l.hp
632
633         html_tile = $("<div class=\"tile hp#{hp}\" style=\"left: #{x}px; top: #{y}px\" unselectable=\"on\">#{letter}</div>")
634         $board.append(html_tile)
635         html_tile.show()
636
637         tile = {
638                 text: letter
639                 dom: html_tile
640                 hp: hp
641                 space: space
642         }
643         space.tile = tile
644
645         html_tile.click ->
646                 return if tile.hp < 1
647                 word = selected_word()
648                 if tile in selected
649                         if selected_word().length > 2 and is_word(word) and tile is selected.last()
650                                 activate_selection()
651                         else
652                                 if selected.length is 1
653                                         unselect_all()
654                                 else
655                                         unselect_all()
656                                         select_tile tile
657                 else # clicked a non-selected tile
658                         if selected.length > 0 and not (tile.space in selected.last().space.neighbors)
659                                 unselect_all()
660                         select_tile tile
661         return tile
662
663 $board = null
664 init_html_board = ->
665         $('#loading').remove()
666         $big_tip = $('#big_tip')
667         $little_tip = $('#little_tip')
668         $score_display = $('#score')
669         $score_display.html score
670         $definition_body = $('#definition_body')
671         $board = $('#board')
672         # make html for board
673         for s in spaces
674                 new_tile s, s.top_px
675
676 word_bins = []; word_bins.push(',') for [0...997]
677 hash_word = (word) ->
678         h = 0
679         for i in [0...word.length]
680                 h ^= word.charCodeAt(i) << ((i*3) % 21)
681         return h % 997
682 is_word = (str) ->
683         word_bins[hash_word str].indexOf(",#{str},") > -1
684
685 # this is called automatically by the compressed wordlist
686 parse_word_list = (compressed) ->
687         prefix = ''
688         cap_a = "A".charCodeAt 0
689         i = 0
690         next_chunk = ->
691                 chunk = compressed[i]
692                 for word in chunk.match(/[a-z]*[A-Z]/g)
693                         # the capital letter (at the end of the match) says how many characters
694                         # from the end of the previous word should be removed to create the prefix
695                         # for the next word. "A" for 0, "B" for 1, "C" for 2, etc
696                         bs = word[word.length - 1].charCodeAt(0) - cap_a
697                         word = prefix + word[0 ... word.length - 1]
698                         word_bins[hash_word word] += word + ','
699                         prefix = word[0 ... word.length - bs]
700                 if ++i is compressed.length
701                         return
702                 else
703                         timeout 1, next_chunk
704         timeout 1, next_chunk
705
706 extract_wiktionary_definiton = (html) ->
707         found = false
708         finds = {}
709         language = false
710         part = false
711
712         # clean HTML
713         ##################
714         # when we instantiate the html so we can use dom traversal, the browser
715         # will start loading images and such. This section attempts to mangle the
716         # html so no resources are loaded when the html is parsed.
717
718         # attributes
719         #                            src: <img>, <audio>, etc
720         #                         onload: only <body>?
721         #   archive,codebase,data,usemap: <object>
722         #                           href: <link>
723         #                 id,class,style: background: url(foo.png), etc
724         html = html.replace /[ ]?[a-z]+=['"][^"']*['"]/ig, '', html
725         html = html.replace /<\/?(audio|source|a|span|table|tr|td|table)>/ig, '', html
726         html = html.replace /\[edit\]/ig, '', html
727
728         elements = $(html)
729
730         valid_parts = ["Abbreviation", "Adjective", "Adverb", "Article", "Cardinal number", "Conjunction", "Determiner", "Interjection", "Noun", "Numeral", "Particle", "Preposition", "Pronoun", "Verb"]
731
732         elements.each (i, el) ->
733                 #which tag: el.tagName
734                 if el.tagName is 'H2'
735                         # if we found a definition in the previous language section, run with it
736                         # (we only stop for verbs, in hopes of finding one in english)
737                         if found
738                                 return false # break
739                         part = false # mark us not being in a definition section unless the next section finds a part of speach header
740                         language = $(el).text()
741                 if language and el.tagName is 'H3' or el.tagName is 'H4' # eg yak def uses one for english and one for dutch
742                         part = false
743                         text = $(el).text()
744                         for p in valid_parts
745                                 if text is "#{p}"
746                                         part = p.toLowerCase()
747                                         # FIXME break
748                 if part and el.tagName is 'OL'
749                         $(el).children().each (i, el) ->
750                                 new_def = $(el).text()
751                                 if new_def.substr(0, 9) is '(obsolete' or new_def.substr(0, 8) is "(archaic" or new_def.substr(0, 20) is "Alternative form of " or new_def.substr(0, 24) is "Alternative spelling of "
752                                         key = 'lame'
753                                 else
754                                         if part is 'verb'
755                                                 key = 'verb'
756                                         else
757                                                 key = 'nonverb'
758                                 finds[key] ?= [part, new_def, language]
759                                 found = true
760                                 if part is 'verb'
761                                         # verbs are the best! stop scanning when we find one
762                                         return false # break
763                         if found.verb
764                                 return false # break
765
766         part_defs = (finds[i] for i in ['verb', 'nonverb', 'lame'] when finds[i])
767         unless part_defs.length
768                 return false
769
770         return part_defs[0]
771
772
773 look_up_definition = (word) ->
774         $definition_body.html "Looking up definition for \"#{word}\"..."
775         $.ajax({
776                 url: "http://en.wiktionary.org/w/api.php?action=parse&format=json&page=#{word}"
777                 jsonpCallback: "lud_#{word}" # always use the same callback for the same word so it's cacheable
778                 dataType: 'jsonp'
779                 cache: true
780                 success: (data, error_msg, xhr) ->
781                         if data?.parse?.text?['*']?
782                                 tdl = extract_wiktionary_definiton data.parse.text['*']
783                                 if tdl
784                                         show_definition word, tdl[0], tdl[1], tdl[2]
785                                 else
786                                         $definition_body.html "Oops, could't find a definition for \"#{word}\"."
787                         else
788                                 $definition_body.html "Sorry, couldn't find a definition for \"#{word}\"."
789         })
790
791 start_over = ->
792         selected = []
793         score = 0
794         $score_display.html score
795         for s in spaces
796                 selected.push s.tile
797         blip_selection()
798         init_booms()
799
800 init_start_over_link = ->
801         $('#start-over').click (event) ->
802                 event.preventDefault()
803                 if confirm "Are you sure you want to start over? There is no undo."
804                         start_over()
805
806 cur_tab = 'instructions'
807 tabtab_height = 20
808 tab_height = 150
809 init_tab = (t) ->
810         $('#' + t).click ->
811                 return if t is cur_tab
812                 $('#' + cur_tab).removeClass('selected-tab').addClass('tab').animate({height: tabtab_height}, 1000)
813                 $('#' + t).removeClass('tab').addClass('selected-tab').animate({height: tab_height}, 1000)
814                 cur_tab = t
815 init_tabs = ->
816         for t in ['instructions', 'definition', 'donate', 'restart']
817                 init_tab t
818
819 init_keybinding = ->
820         $(window).keydown (e) ->
821                 switch e.keyCode
822                         when 32, 10, 13
823                                 activate_selection()
824                         when 27
825                                 unselect_all()
826
827 log = (args...) ->
828         console.log args... if console?.log?
829
830 init_game = ->
831         if $(window).height() >= 440
832                 $('#centerer').css('margin-top', '25px')
833         init_keybinding()
834         init_tabs()
835         init_board()
836         init_html_board()
837         init_start_over_link()
838         update_selection_display()
839         init_booms()
840
841 $(init_game)