JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
459f152b95f323cedece996784d8744c1ea9fe1d
[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                                 if tile.new_hp
552                                         # for overlap of board and [n]neigbors
553                                         tile.new_hp += force
554                                 else
555                                         tile.new_hp = tile.hp + force
556         for s in spaces
557                 s.tile.new_hp ?= s.tile.hp - 1
558                 if s.tile.new_hp < 0
559                         s.tile.new_hp = 0
560                 else if s.tile.new_hp > HP_MAX
561                         s.tile.new_hp = HP_MAX
562                 if s.tile.new_hp isnt s.tile.hp
563                         s.tile.dom.removeClass "hp#{s.tile.hp}"
564                         s.tile.dom.addClass "hp#{s.tile.new_hp}"
565                         s.tile.hp = s.tile.new_hp
566                 delete s.tile.new_hp
567         timeout fade_ms + 1, ->
568                 # delete old tiles, mark where tiles are moving
569                 for fader in faders
570                         fader.space.column.fader_count += 1
571                         fader.dom.remove()
572                         fader.removed = true
573                         for above in fader.space.aboves
574                                 if above.tile.dest?
575                                         above.tile.dest += 1
576                                 else
577                                         above.tile.dest = above.id + 1
578
579                 # move tiles down (graphically and in data structure)
580                 rspaces = []
581                 for s in spaces
582                         rspaces.unshift s
583                 for space in rspaces
584                         tile = space.tile
585                         if tile.dest? and not (tile.removed?)
586                                 dest_space = spaces[tile.dest]
587                                 delete tile.dest
588                                 tile.dom.animate {top: "#{dest_space.top_px}px"}, slide_ms
589                                 tile.space = dest_space
590                                 dest_space.tile = tile
591
592                 # create new tiles
593                 for column in columns
594                         dest = 0
595                         while column.fader_count > 0
596                                 column.fader_count -= 1
597                                 slide_from = -10 - tile_width
598                                 slide_from -= (50 + tile_width) * column.fader_count
599                                 space = column.spaces[dest++]
600                                 tile = new_tile space, slide_from
601                                 tile.dom.animate {top: "#{space.top_px}px"}, slide_ms
602
603                 save_game()
604
605 score_for = (word) -> Math.round(Math.pow(1.7, word.length))
606
607 activate_selection = ->
608         word = selected_word()
609         if word.length < 3
610                 # should only happen when trying to blip a word with the keyboard
611                 # FIXME make this a hint
612                 log "Too short: \"#{word}\""
613                 return
614         unless is_word word
615                 # should only happen when trying to blip a word with the keyboard
616                 # FIXME make this automatically part of the selection display
617                 log "Not on word list: \"#{word}\""
618                 return
619         word_score = score_for word
620         score += word_score
621         $score_display.html score
622         # FIXME make some kind of animation showing score gain
623         blip_selection()
624         look_up_definition word
625         $('#definition').click()
626
627
628 show_definition = (word, type, definition, language) ->
629         html = "<a href=\"http://en.wiktionary.org/wiki/#{word}\" target=\"_blank\">"
630         html += "#{word.substr(0, 1).toUpperCase() + word.substr(1)}</a>, #{type}"
631         if language isnt 'English'
632                 html += " (#{language})"
633         html += ': '
634         html += definition
635         html += '<div id="definition_credit">Definition &copy;<a href="http://en.wiktionary.org/" target="_blank">wiktionary.org</a> CC-BY-SA</div>'
636         $definition_body.html html
637
638 connector_width = 11
639 connector_radius = 5
640 connector_slant = 12
641 add_connector = (tile, horiz, vert) ->
642         style = {}
643         switch horiz
644                 when 'left'
645                         style.right = '100%'
646                 when 'mid'
647                         style.left = 21 - connector_radius
648                 when 'right'
649                         style.left = '100%'
650         switch vert
651                 when 'top'
652                         style.bottom = '100%'
653                 when 'up'
654                         style.top = 21 - connector_radius - connector_slant
655                 when 'down'
656                         style.top = 21 - connector_radius + connector_slant
657                 when 'bot'
658                         style.top = '100%'
659         tile.connector = $("<div class=\"connector\"></div>").css style
660         tile.dom.append tile.connector
661
662 select_tile = (tile) ->
663         selected.push tile
664         if selected.length > 1
665                 prev = selected[selected.length - 2]
666                 if prev.space.top_px < tile.space.top_px
667                         if prev.space.left_px < tile.space.left_px
668                                 add_connector tile, 'left', 'up'
669                         else if prev.space.left_px is tile.space.left_px
670                                 add_connector tile, 'mid', 'top'
671                         else
672                                 add_connector tile, 'right', 'up'
673                 else
674                         if prev.space.left_px < tile.space.left_px
675                                 add_connector tile, 'left', 'down'
676                         else if prev.space.left_px is tile.space.left_px
677                                 add_connector tile, 'mid', 'bot'
678                         else
679                                 add_connector tile, 'right', 'down'
680         update_selection_display()
681
682 new_tile = (space, y) ->
683         x = space.left_px
684         l = new_letter()
685         letter = l.letter
686         hp = l.hp
687
688         html_tile = $("<div class=\"tile hp#{hp}\" style=\"left: #{x}px; top: #{y}px\" unselectable=\"on\">#{letter}</div>")
689         $board.append(html_tile)
690         html_tile.show()
691
692         tile = {
693                 text: letter
694                 dom: html_tile
695                 hp: hp
696                 space: space
697         }
698         space.tile = tile
699
700         html_tile.click ->
701                 return unselect_all() if tile.hp < 1
702                 word = selected_word()
703                 if tile in selected
704                         if selected_word().length > 2 and is_word(word) and tile is selected.last()
705                                 activate_selection()
706                         else
707                                 if selected.length is 1
708                                         unselect_all()
709                                 else
710                                         unselect_all()
711                                         select_tile tile
712                 else # clicked a non-selected tile
713                         if selected.length > 0 and not (tile.space in selected.last().space.neighbors)
714                                 unselect_all()
715                         select_tile tile
716         return tile
717
718 $board = null
719 init_html_board = ->
720         $('#loading').remove()
721         $big_tip = $('#big_tip')
722         $little_tip = $('#little_tip')
723         $score_display = $('#score')
724         $score_display.html score
725         $definition_body = $('#definition_body')
726         $board = $('#board')
727         # make html for board
728         for s in spaces
729                 new_tile s, s.top_px
730
731 word_bins = []; word_bins.push(',') for [0...997]
732 hash_word = (word) ->
733         h = 0
734         for i in [0...word.length]
735                 h ^= word.charCodeAt(i) << ((i*3) % 21)
736         return h % 997
737 is_word = (str) ->
738         word_bins[hash_word str].indexOf(",#{str},") > -1
739
740 # this is called automatically by the compressed wordlist
741 parse_word_list = (compressed) ->
742         prefix = ''
743         cap_a = "A".charCodeAt 0
744         i = 0
745         next_chunk = ->
746                 chunk = compressed[i]
747                 for word in chunk.match(/[a-z]*[A-Z]/g)
748                         # the capital letter (at the end of the match) says how many characters
749                         # from the end of the previous word should be removed to create the prefix
750                         # for the next word. "A" for 0, "B" for 1, "C" for 2, etc
751                         bs = word[word.length - 1].charCodeAt(0) - cap_a
752                         word = prefix + word[0 ... word.length - 1]
753                         word_bins[hash_word word] += word + ','
754                         prefix = word[0 ... word.length - bs]
755                 if ++i is compressed.length
756                         return
757                 else
758                         timeout 1, next_chunk
759         timeout 1, next_chunk
760
761 extract_wiktionary_definiton = (html) ->
762         found = false
763         finds = {}
764         language = false
765         part = false
766
767         # clean HTML
768         ##################
769         # when we instantiate the html so we can use dom traversal, the browser
770         # will start loading images and such. This section attempts to mangle the
771         # html so no resources are loaded when the html is parsed.
772
773         # attributes
774         #                            src: <img>, <audio>, etc
775         #                         onload: only <body>?
776         #   archive,codebase,data,usemap: <object>
777         #                           href: <link>
778         #                 id,class,style: background: url(foo.png), etc
779         html = html.replace /[ ]?[a-z]+=['"][^"']*['"]/ig, '', html
780         html = html.replace /<\/?(audio|source|a|span|table|tr|td|table)>/ig, '', html
781         html = html.replace /\[edit\]/ig, '', html
782
783         elements = $(html)
784
785         valid_parts = ["Abbreviation", "Adjective", "Adverb", "Article", "Cardinal number", "Conjunction", "Determiner", "Interjection", "Noun", "Numeral", "Particle", "Preposition", "Pronoun", "Verb"]
786
787         elements.each (i, el) ->
788                 #which tag: el.tagName
789                 if el.tagName is 'H2'
790                         # if we found a definition in the previous language section, run with it
791                         # (we only stop for verbs, in hopes of finding one in english)
792                         if found
793                                 return false # break
794                         part = false # mark us not being in a definition section unless the next section finds a part of speach header
795                         language = $(el).text()
796                 if language and el.tagName is 'H3' or el.tagName is 'H4' # eg yak def uses one for english and one for dutch
797                         part = false
798                         text = $(el).text()
799                         for p in valid_parts
800                                 if text is "#{p}"
801                                         part = p.toLowerCase()
802                                         # FIXME break
803                 if part and el.tagName is 'OL'
804                         $(el).children().each (i, el) ->
805                                 new_def = $(el).text()
806                                 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 "
807                                         key = 'lame'
808                                 else
809                                         if part is 'verb'
810                                                 key = 'verb'
811                                         else
812                                                 key = 'nonverb'
813                                 finds[key] ?= [part, new_def, language]
814                                 found = true
815                                 if part is 'verb'
816                                         # verbs are the best! stop scanning when we find one
817                                         return false # break
818                         if found.verb
819                                 return false # break
820
821         part_defs = (finds[i] for i in ['verb', 'nonverb', 'lame'] when finds[i])
822         unless part_defs.length
823                 return false
824
825         return part_defs[0]
826
827
828 look_up_definition = (word) ->
829         $definition_body.html "Looking up definition for \"#{word}\"..."
830         $.ajax({
831                 url: "http://en.wiktionary.org/w/api.php?action=parse&format=json&page=#{word}"
832                 jsonpCallback: "lud_#{word}" # always use the same callback for the same word so it's cacheable
833                 dataType: 'jsonp'
834                 cache: true
835                 success: (data, error_msg, xhr) ->
836                         if data?.parse?.text?['*']?
837                                 tdl = extract_wiktionary_definiton data.parse.text['*']
838                                 if tdl
839                                         show_definition word, tdl[0], tdl[1], tdl[2]
840                                 else
841                                         $definition_body.html "Oops, could't find a definition for \"#{word}\"."
842                         else
843                                 $definition_body.html "Sorry, couldn't find a definition for \"#{word}\"."
844         })
845
846 start_over = ->
847         selected = []
848         score = 0
849         difficulty_level = 0
850         next_level_at = 200
851         $score_display.html score
852         for s in spaces
853                 selected.push s.tile
854         blip_selection()
855
856 init_start_over_link = ->
857         $('#start-over').click (event) ->
858                 event.preventDefault()
859                 if confirm "Are you sure you want to start over? There is no undo."
860                         start_over()
861
862 cur_tab = 'instructions'
863 tabtab_height = 20
864 tab_height = 150
865 init_tab = (t) ->
866         $('#' + t).click ->
867                 return if t is cur_tab
868                 $('#' + cur_tab).removeClass('selected-tab').addClass('tab').animate({height: tabtab_height}, 1000)
869                 $('#' + t).removeClass('tab').addClass('selected-tab').animate({height: tab_height}, 1000)
870                 cur_tab = t
871 init_tabs = ->
872         for t in ['instructions', 'definition', 'donate', 'restart']
873                 init_tab t
874
875 init_keybinding = ->
876         $(window).keydown (e) ->
877                 switch e.keyCode
878                         when 32, 10, 13
879                                 activate_selection()
880                         when 27
881                                 unselect_all()
882
883 log = (args...) ->
884         console.log args... if console?.log?
885
886 init_game = ->
887         if $(window).height() >= 440
888                 $('#centerer').css('margin-top', '25px')
889         init_keybinding()
890         init_tabs()
891         init_board()
892         init_html_board()
893         init_start_over_link()
894         update_selection_display()
895
896 $(init_game)