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