# HexBog, a word game # Copyright (C) 2012 Jason Woofenden # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ############################################## ############## settings ################ ############################################## tile_radius = 26 tile_width = tile_radius * 2 fade_ms = 400 slide_ms = 2000 board_col_heights = [5, 6, 7, 8, 7, 6, 5] ############################################################## ############## fix javascript some more ################ ############################################################## # so annoying that setTimeout has its arguments in the wrong order timeout = (ms, callback) -> setTimeout callback, ms # warning: it's shalow (sub-elements are not cloned) Array::clone = -> return this.slice(0) Array::sum = -> ret = 0 ret += i for i in this return ret # ascending. All values must be Numbers Array::num_sort = -> return this.sort((a, b) -> return a - b) Array::last = -> return this[this.length - 1] ############################################################## ############## cookies (auto-save game) ################ ############################################################## set_cookie = (name, value, days) -> date = new Date() date.setTime date.getTime()+(days*24*60*60*1000) cookie = "#{name}=#{value}; expires=#{date.toGMTString()}; path=/" document.cookie = cookie window.sc = set_cookie get_cookie = (name) -> key = name + '=' for c in document.cookie.split /; */ if c.indexOf key is 0 return c.substr key.length return null delete_cookie = (name) -> set_cookie name, '', -1 window.dc = delete_cookie board_cols = board_col_heights.length board_tiles = board_col_heights.sum() board_col_top_px = [] score = 0 board = new Array(board_tiles) # letters ("Qu" or single letter) board_neighbors = [] # array of tile numbers "next to" this one tile_top_px = [] # array of pixel coordinates for top of column board_left_px = [] # array of pixel coordinates for left of column board_aboves = [] # array of tile numbers above, starting from top board_below = [] # tile number of next tile below or false selected = [] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" letter_distribution = [ 14355 # a 3968 # b 6325 # c 7045 # d 20258 # e 2739 # f 5047 # g 4372 # h 13053 # i 516 # j 2600 # k 9631 # l 5115 # m 10082 # n 11142 # o 5292 # p 287 # qu 12341 # r 16571 # s 10215 # t 6131 # u 1728 # v 2184 # w 619 # x 3512 # y 831 # z ] letter_distribution_total = 175973 # letter_distribution.sum() new_letter_queue = [] new_letter = -> if new_letter_queue.length l = new_letter_queue.shift() if l is 'Q' return 'Qu' else return l r = Math.floor Math.random() * (letter_distribution_total + 1) for i in [0..25] r -= letter_distribution[i] if r <= 0 if letters[i] is 'Q' return 'Qu' return letters[i] return 'Z' # just in case # in memory it's layed out like this: # a c f j m # b d g k n # e h l # i # for display, columns are slid vertically like so: # f # c j # a g m # d k # b h n # e l # i # # work out which grid spaces are connected init_board_layout = () -> col_offset = 0 middle_col = (board_cols - 1) / 2 # how many tiles before the current tile? for col_num in [0 .. board_cols - 1] if col_num < middle_col fw_other = 1 else fw_other = -1 if col_num > middle_col bw_other = 1 else bw_other = -1 is_first_col = col_num is 0 is_last_col = col_num is board_cols - 1 neighbors = [] push = (offset) -> neighbors.push col_offset + offset col_top_px = Math.abs col_num - middle_col col_top_px *= tile_radius board_col_top_px.push col_top_px above = [] for i in [0 .. board_col_heights[col_num] - 1] is_top_tile = i is 0 is_bottom_tile = i is board_col_heights[col_num] - 1 # link tile number to pixel "top" and "left" of containing column tile_top_px.push col_top_px + i * tile_width board_left_px.push col_num * tile_width # aboves (array of tile numbers above, starting from top) board_aboves.push above.clone() above.push i + col_offset # below (SINGLE tile number of tile below or false) if is_bottom_tile board_below.push false else board_below.push col_offset + i + 1 # neighbors (array of tile numbers "next to" this one) neighbors = [] unless is_top_tile # upward link push i - 1 unless is_bottom_tile # downward links push i + 1 unless is_first_col # leftward links unless is_bottom_tile and bw_other is -1 push i - board_col_heights[col_num - 1] unless is_top_tile and bw_other is -1 push i - board_col_heights[col_num - 1] + bw_other unless is_last_col # rightward links unless is_bottom_tile and fw_other is -1 push i + board_col_heights[col_num] unless is_top_tile and fw_other is -1 push i + board_col_heights[col_num] + fw_other board_neighbors.push neighbors.clone() col_offset += board_col_heights[col_num] init_board = -> encoded = window.location.hash if encoded? and encoded.charAt 0 is '#' encoded = encoded.substr 1 unless encoded? and encoded.length > board_tiles encoded = get_cookie 'hexbog' if encoded? and encoded.length > board_tiles new_letter_queue = (encoded.substr 0, board_tiles).split '' score = parseInt(encoded.substr(board_tiles), 10) # work out which grid spaces are connected # (neighbors, above, down) init_board_layout() $big_tip = null # initialized by init_html_board $little_tip = null # initialized by init_html_board $score_display = null # initialized by init_html_board $definition_body = null # initialized by init_html_board update_selection_display = -> word = selected_word() $big_tip.removeClass('good') if word.length > 0 $big_tip.html word if word.length < 3 $little_tip.html "Click more tiles (3 minimum)" else if is_word word if word.indexOf(word.substr(word.length - 1)) < word.length - 1 last = 'last ' else last = '' $little_tip.html "Click the #{last}\"#{word.substr(word.length - 1)}\" for #{score_for word} points" $big_tip.addClass('good') else $little_tip.html "\"#{word}\" is not in the word list." else $big_tip.html "← Click a word" $little_tip.html "(tiles must be touching)" # color the selected tiles according to whether they're a word or not if word.length classes = ['selected_word', 'selected'] if is_word word c = 0 else c = 1 for num in selected html_tiles[num].addClass classes[c] html_tiles[num].removeClass classes[1 - c] # unselects the last tile of the selecetion unselect_tile = -> _unselect_tile() update_selection_display() _unselect_tile = -> num = selected.pop() html_tile = html_tiles[num] html_tile.removeClass 'selected_word' html_tile.removeClass 'selected' unselect_all = -> while selected.length _unselect_tile() update_selection_display() shrink_selection = (leave_count) -> while selected.length > leave_count _unselect_tile() update_selection_display() selected_word = -> word = '' word += board[i] for i in selected return word.toLowerCase() save_game = -> encoded = '' for t in board encoded += t.substr 0, 1 encoded += score set_cookie 'hexbog', encoded, 365 window.location.hash = encoded # remove the selected tiles from the board, create new tiles, and slide everything into place blip_selection = -> faders = selected.num_sort() selected = [] update_selection_display() for i in faders html_tiles[i].unbind('click').fadeOut fade_ms timeout fade_ms + 1, -> # which tiles need to be slid down sliders = (false for i in board) prev_col_top = null next_new_y = null for deleted in faders # find the tile number of the top tile in this column if board_aboves[deleted].length is 0 col_top = deleted else col_top = board_aboves[deleted][0] # reset location where new tiles appear when we change columns if prev_col_top isnt col_top next_new_y = -10 - tile_width prev_col_top = col_top html_tiles[deleted].remove() # For each each tile above the one we've deleted: # 1. move it down one slot in the data scructures # 2. mark it as needing to slide dest = deleted aboves = board_aboves[deleted].clone().reverse() for above in aboves html_tiles[dest] = html_tiles[above] html_tiles[dest].data 'tile_number', dest board[dest] = board[above] sliders[dest] = true --dest sliders[col_top] = true # the new tile needs to be slid down too new_tile col_top, board_left_px[col_top], next_new_y next_new_y -= tile_width + 50 for slide, i in sliders if slide html_tiles[i].animate {top: "#{tile_top_px[i]}px"}, slide_ms sliders[i] = false save_game() score_for = (word) -> Math.round(Math.pow(1.7, word.length)) activate_selection = -> word = selected_word() if word.length < 3 # FIXME make this a hint log "Too short: \"#{word}\"" return unless is_word word # FIXME make this automatically part of the selection display log "Not on word list: \"#{word}\"" return word_score = score_for word score += word_score $score_display.html score # FIXME make some kind of animation showing score gain log "blipped \"#{word}\" for #{word_score} points" blip_selection() look_up_definition word $('#definition').click() show_definition = (word, type, definition, language) -> html = "" html += "#{word.substr(0, 1).toUpperCase() + word.substr(1)}, #{type}" if language isnt 'English' html += " (#{language})" html += ': ' html += definition html += '
Definition ©wiktionary.org CC-BY-SA
' $definition_body.html html select_tile = (num) -> html_tile = html_tiles[num] # html_tile.css backgroundColor: tile_selected_color selected.push num update_selection_display() return new_tile = (num, x, y) -> letter = new_letter() html_tile = $("
#{letter}
") $board.append(html_tile) html_tile.data 'tile_number', num board[num] = letter html_tiles[num] = html_tile html_tile.click -> me = $(this) num = me.data 'tile_number' if num in selected nth_of_word = selected.indexOf(num) first = nth_of_word is 0 last = nth_of_word is selected.length - 1 if first and last unselect_all() # Clicking only selected letter unselects it else if first and !last shrink_selection 1 # Clicking start of word goes back to just that letter # should this unselect all? else if last activate_selection() else shrink_selection nth_of_word + 1 else # (not clicking on selected tile) if selected.length is 0 select_tile num else unless num in board_neighbors[selected.last()] unselect_all() select_tile num $board = null html_tiles = [] init_html_board = -> $('#loading').remove() $big_tip = $('#big_tip') $little_tip = $('#little_tip') $score_display = $('#score') $score_display.html score $definition_body = $('#definition_body') $board = $('#board') # make html for board tile_number = 0 for col_num in [0 .. board_cols - 1] for num in [0 .. board_col_heights[col_num] - 1] x = col_num * tile_width y = board_col_top_px[col_num] + num * tile_width new_tile tile_number, x, y tile_number++ word_bins = []; word_bins.push(',') for [0...997] hash_word = (word) -> h = 0 for i in [0...word.length] h ^= word.charCodeAt(i) << ((i*3) % 21) return h % 997 is_word = (str) -> word_bins[hash_word str].indexOf(",#{str},") > -1 # this is called automatically by the compressed wordlist parse_word_list = (compressed) -> prefix = '' cap_a = "A".charCodeAt 0 i = 0 next_chunk = -> chunk = compressed[i] for word in chunk.match(/[a-z]*[A-Z]/g) # the capital letter (at the end of the match) says how many characters # from the end of the previous word should be removed to create the prefix # for the next word. "A" for 0, "B" for 1, "C" for 2, etc bs = word[word.length - 1].charCodeAt(0) - cap_a word = prefix + word[0 ... word.length - 1] word_bins[hash_word word] += word + ',' prefix = word[0 ... word.length - bs] if ++i is compressed.length return else timeout 1, next_chunk timeout 1, next_chunk extract_wiktionary_definiton = (html) -> found = false finds = {} language = false part = false # clean HTML ################## # when we instantiate the html so we can use dom traversal, the browser # will start loading images and such. This section attempts to mangle the # html so no resources are loaded when the html is parsed. # attributes # src: ,