JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
tweak graphics
[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 # remove the selected tiles from the board, create new tiles, and slide everything into place
374 blip_selection = ->
375         word_length = selected_word().length
376         faders = selected
377         selected = []
378         update_selection_display()
379         neighbors = {}
380         nneighbors = {}
381         for tile in faders
382                 tile.dom.unbind('click').fadeOut fade_ms
383                 tile.new_hp = tile.hp
384                 for n in tile.space.neighbors
385                         neighbors[n.id] = n.tile
386                         for nn in n.neighbors
387                                 nneighbors[nn.id] = nn.tile
388         # fix overlaps of faders, neighors, nneighbors
389         for tile in faders
390                 delete nneighbors[tile.space.id]
391                 delete neighbors[tile.space.id]
392         for k, v of neighbors
393                 delete nneighbors[k]
394         # convert to arrays so we can sort, etc
395         nneighbors = (v for k, v of nneighbors)
396         neighbors = (v for k, v of neighbors)
397         boom = [
398                 {
399                         tiles: neighbors,
400                         up: [],
401                         down: []
402                 },
403                 {
404                         tiles: nneighbors,
405                         up: [],
406                         down: []
407                 }
408         ]
409         for n in boom
410                 for t in n.tiles
411                         if t.hp is 0
412                                 n.down.push t
413                         else
414                                 n.up.push t
415         switch word_length
416                 when 3
417                         boom[0].flips = 1
418                         boom[0].force = 2
419                         boom[1].flips = 0
420                         boom[1].force = 0
421                 when 4
422                         boom[0].flips = 'all'
423                         boom[0].force = 4
424                         boom[1].flips = 0
425                         boom[1].force = 2
426                 when 5
427                         boom[0].flips = 'all'
428                         boom[0].force = 6
429                         boom[1].flips = 2
430                         boom[1].force = 4
431                 when 6
432                         boom[0].flips = 'all'
433                         boom[0].force = 10
434                         boom[1].flips = 5
435                         boom[1].force = 6
436                 when 7
437                         boom[0].flips = 'all'
438                         boom[0].force = 10
439                         boom[1].flips = 'all'
440                         boom[1].force = 10
441                 else
442                         boom[0].flips = 0
443                         boom[0].force = 0
444                         boom[1].flips = 0
445                         boom[1].force = 0
446                         # unsink/heal the whole board
447                         for s in spaces
448                                 if s.tile.hp is 0
449                                         unsink s.tile
450                                 else
451                                         s.tile.new_hp = 10
452         for b in boom
453                 if b.flips is 'all' or b.flips >= b.down.length
454                         for t in b.down
455                                 unsink t
456                 else
457                         down_count = b.down.length
458                         while b.flips > 0 and down_count
459                                 b.flips -= 1
460                                 flipper = Math.floor(Math.random() * down_count)
461                                 unsink b.down[flipper]
462                                 down_count -= 1
463                                 # move the last tile back into range
464                                 b.down[flipper] = b.down[down_count]
465                 if b.force > 0
466                         for t in b.up
467                                 t.new_hp = t.hp + b.force
468         for s in spaces
469                 s.tile.new_hp ?= s.tile.hp - 1
470                 if s.tile.new_hp < 0
471                         s.tile.new_hp = 0
472                 else if s.tile.new_hp > HP_MAX
473                         s.tile.new_hp = HP_MAX
474                 if s.tile.new_hp isnt s.tile.hp
475                         s.tile.dom.removeClass "hp#{s.tile.hp}"
476                         s.tile.dom.addClass "hp#{s.tile.new_hp}"
477                         s.tile.hp = s.tile.new_hp
478                 delete s.tile.new_hp
479         timeout fade_ms + 1, ->
480                 # delete old tiles, mark where tiles are moving
481                 for fader in faders
482                         fader.space.column.fader_count += 1
483                         fader.dom.remove()
484                         fader.removed = true
485                         for above in fader.space.aboves
486                                 if above.tile.dest?
487                                         above.tile.dest += 1
488                                 else
489                                         above.tile.dest = above.id + 1
490
491                 # move tiles down (graphically and in data structure)
492                 rspaces = []
493                 for s in spaces
494                         rspaces.unshift s
495                 for space in rspaces
496                         tile = space.tile
497                         if tile.dest? and not (tile.removed?)
498                                 dest_space = spaces[tile.dest]
499                                 delete tile.dest
500                                 tile.dom.animate {top: "#{dest_space.top_px}px"}, slide_ms
501                                 tile.space = dest_space
502                                 dest_space.tile = tile
503
504                 # create new tiles
505                 for column in columns
506                         dest = 0
507                         while column.fader_count > 0
508                                 column.fader_count -= 1
509                                 slide_from = -10 - tile_width
510                                 slide_from -= (50 + tile_width) * column.fader_count
511                                 space = column.spaces[dest++]
512                                 tile = new_tile space, slide_from
513                                 tile.dom.animate {top: "#{space.top_px}px"}, slide_ms
514
515                 save_game()
516
517 score_for = (word) -> Math.round(Math.pow(1.7, word.length))
518
519 activate_selection = ->
520         word = selected_word()
521         if word.length < 3
522                 # FIXME make this a hint
523                 log "Too short: \"#{word}\""
524                 return
525         unless is_word word
526                 # FIXME make this automatically part of the selection display
527                 log "Not on word list: \"#{word}\""
528                 return
529         word_score = score_for word
530         score += word_score
531         $score_display.html score
532         # FIXME make some kind of animation showing score gain
533         blip_selection()
534         look_up_definition word
535         $('#definition').click()
536
537
538 show_definition = (word, type, definition, language) ->
539         html = "<a href=\"http://en.wiktionary.org/wiki/#{word}\" target=\"_blank\">"
540         html += "#{word.substr(0, 1).toUpperCase() + word.substr(1)}</a>, #{type}"
541         if language isnt 'English'
542                 html += " (#{language})"
543         html += ': '
544         html += definition
545         html += '<div id="definition_credit">Definition &copy;<a href="http://en.wiktionary.org/" target="_blank">wiktionary.org</a> CC-BY-SA</div>'
546         $definition_body.html html
547
548
549 select_tile = (tile) ->
550         selected.push tile
551         update_selection_display()
552
553 new_tile = (space, y) ->
554         x = space.left_px
555         l = new_letter()
556         letter = l.letter
557         hp = l.hp
558
559         html_tile = $("<div class=\"tile hp#{hp}\" style=\"left: #{x}px; top: #{y}px\" unselectable=\"on\">#{letter}</div>")
560         $board.append(html_tile)
561         html_tile.show()
562
563         tile = {
564                 text: letter
565                 dom: html_tile
566                 hp: hp
567                 space: space
568         }
569         space.tile = tile
570
571         html_tile.click ->
572                 return if tile.hp < 1
573                 word = selected_word()
574                 if tile in selected
575                         if selected_word().length > 2 and is_word(word) and tile is selected.last()
576                                 activate_selection()
577                         else
578                                 if selected.length is 1
579                                         unselect_all()
580                                 else
581                                         unselect_all()
582                                         select_tile tile
583                 else # clicked a non-selected tile
584                         if selected.length > 0 and not (tile.space in selected.last().space.neighbors)
585                                 unselect_all()
586                         select_tile tile
587         return tile
588
589 $board = null
590 init_html_board = ->
591         $('#loading').remove()
592         $big_tip = $('#big_tip')
593         $little_tip = $('#little_tip')
594         $score_display = $('#score')
595         $score_display.html score
596         $definition_body = $('#definition_body')
597         $board = $('#board')
598         # make html for board
599         for s in spaces
600                 new_tile s, s.top_px
601
602 word_bins = []; word_bins.push(',') for [0...997]
603 hash_word = (word) ->
604         h = 0
605         for i in [0...word.length]
606                 h ^= word.charCodeAt(i) << ((i*3) % 21)
607         return h % 997
608 is_word = (str) ->
609         word_bins[hash_word str].indexOf(",#{str},") > -1
610
611 # this is called automatically by the compressed wordlist
612 parse_word_list = (compressed) ->
613         prefix = ''
614         cap_a = "A".charCodeAt 0
615         i = 0
616         next_chunk = ->
617                 chunk = compressed[i]
618                 for word in chunk.match(/[a-z]*[A-Z]/g)
619                         # the capital letter (at the end of the match) says how many characters
620                         # from the end of the previous word should be removed to create the prefix
621                         # for the next word. "A" for 0, "B" for 1, "C" for 2, etc
622                         bs = word[word.length - 1].charCodeAt(0) - cap_a
623                         word = prefix + word[0 ... word.length - 1]
624                         word_bins[hash_word word] += word + ','
625                         prefix = word[0 ... word.length - bs]
626                 if ++i is compressed.length
627                         return
628                 else
629                         timeout 1, next_chunk
630         timeout 1, next_chunk
631
632 extract_wiktionary_definiton = (html) ->
633         found = false
634         finds = {}
635         language = false
636         part = false
637
638         # clean HTML
639         ##################
640         # when we instantiate the html so we can use dom traversal, the browser
641         # will start loading images and such. This section attempts to mangle the
642         # html so no resources are loaded when the html is parsed.
643
644         # attributes
645         #                            src: <img>, <audio>, etc
646         #                         onload: only <body>?
647         #   archive,codebase,data,usemap: <object>
648         #                           href: <link>
649         #                 id,class,style: background: url(foo.png), etc
650         html = html.replace /[ ]?[a-z]+=['"][^"']*['"]/ig, '', html
651         html = html.replace /<\/?(audio|source|a|span|table|tr|td|table)>/ig, '', html
652         html = html.replace /\[edit\]/ig, '', html
653
654         elements = $(html)
655
656         valid_parts = ["Abbreviation", "Adjective", "Adverb", "Article", "Cardinal number", "Conjunction", "Determiner", "Interjection", "Noun", "Numeral", "Particle", "Preposition", "Pronoun", "Verb"]
657
658         elements.each (i, el) ->
659                 #which tag: el.tagName
660                 if el.tagName is 'H2'
661                         # if we found a definition in the previous language section, run with it
662                         # (we only stop for verbs, in hopes of finding one in english)
663                         if found
664                                 return false # break
665                         part = false # mark us not being in a definition section unless the next section finds a part of speach header
666                         language = $(el).text()
667                 if language and el.tagName is 'H3' or el.tagName is 'H4' # eg yak def uses one for english and one for dutch
668                         part = false
669                         text = $(el).text()
670                         for p in valid_parts
671                                 if text is "#{p}"
672                                         part = p.toLowerCase()
673                                         # FIXME break
674                 if part and el.tagName is 'OL'
675                         $(el).children().each (i, el) ->
676                                 new_def = $(el).text()
677                                 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 "
678                                         key = 'lame'
679                                 else
680                                         if part is 'verb'
681                                                 key = 'verb'
682                                         else
683                                                 key = 'nonverb'
684                                 finds[key] ?= [part, new_def, language]
685                                 found = true
686                                 if part is 'verb'
687                                         # verbs are the best! stop scanning when we find one
688                                         return false # break
689                         if found.verb
690                                 return false # break
691
692         part_defs = (finds[i] for i in ['verb', 'nonverb', 'lame'] when finds[i])
693         unless part_defs.length
694                 return false
695
696         return part_defs[0]
697
698
699 look_up_definition = (word) ->
700         $definition_body.html "Looking up definition for \"#{word}\"..."
701         $.ajax({
702                 url: "http://en.wiktionary.org/w/api.php?action=parse&format=json&page=#{word}"
703                 jsonpCallback: "lud_#{word}" # always use the same callback for the same word so it's cacheable
704                 dataType: 'jsonp'
705                 cache: true
706                 success: (data, error_msg, xhr) ->
707                         if data?.parse?.text?['*']?
708                                 tdl = extract_wiktionary_definiton data.parse.text['*']
709                                 if tdl
710                                         show_definition word, tdl[0], tdl[1], tdl[2]
711                                 else
712                                         $definition_body.html "Oops, could't find a definition for \"#{word}\"."
713                         else
714                                 $definition_body.html "Sorry, couldn't find a definition for \"#{word}\"."
715         })
716
717 start_over = ->
718         selected = []
719         score = 0
720         $score_display.html score
721         for s in spaces
722                 selected.push s.tile
723         blip_selection()
724
725 init_start_over_link = ->
726         $('#start-over').click (event) ->
727                 event.preventDefault()
728                 if confirm "Are you sure you want to start over? There is no undo."
729                         start_over()
730
731 cur_tab = 'instructions'
732 tabtab_height = 20
733 tab_height = 150
734 init_tab = (t) ->
735         $('#' + t).click ->
736                 return if t is cur_tab
737                 $('#' + cur_tab).removeClass('selected-tab').addClass('tab').animate({height: tabtab_height}, 1000)
738                 $('#' + t).removeClass('tab').addClass('selected-tab').animate({height: tab_height}, 1000)
739                 cur_tab = t
740 init_tabs = ->
741         for t in ['instructions', 'definition', 'donate', 'restart']
742                 init_tab t
743
744 init_keybinding = ->
745         $(window).keydown (e) ->
746                 switch e.keyCode
747                         when 32, 10, 13
748                                 activate_selection()
749                         when 27
750                                 unselect_all()
751
752 log = (args...) ->
753         console.log args... if console?.log?
754
755 init_game = ->
756         if $(window).height() >= 440
757                 $('#centerer').css('margin-top', '25px')
758         init_keybinding()
759         init_tabs()
760         init_board()
761         init_html_board()
762         init_start_over_link()
763         update_selection_display()
764
765 $(init_game)