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