Last active
April 12, 2018 03:42
-
-
Save msaari/f694c694f7ea58d283586b15464e3b6b to your computer and use it in GitHub Desktop.
Word game solver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| $begin_time = microtime( true ); | |
| $material = <<<EOF | |
| N1 E1 F3 W3 J4 | |
| S1 E1 O1 I1 H3 | |
| B2 V3 Y3 T1 N1 | |
| G2 R1 O1 T1 D2 | |
| U1 A1 D2 L1 M2 | |
| EOF; | |
| $bag = <<<BAG | |
| E1 U1 N1 M2 R1 E1 G2 Y3 O1 O1 U1 D2 O1 L1 X4 I1 R1 A1 I1 H3 L1 E1 E1 | |
| BAG; | |
| $my_total_score = 191.5; | |
| $their_total_score = 107; | |
| $it_is_my_turn = true; | |
| $original_my_score = $my_total_score; | |
| $original_their_score = $their_total_score; | |
| $original_turn = $it_is_my_turn; | |
| $material = trim( strtolower( $material ) ); | |
| $rows = explode( "\n", $material ); | |
| $table = array(); | |
| $row_counter = 0; | |
| $wild_card = false; | |
| foreach ( $rows as $row ) { | |
| $cell_counter = 0; | |
| $cells = explode( "\t", $row ); | |
| foreach ( $cells as $cell ) { | |
| $letter = substr( $cell, 0, 1 ); | |
| if ( $letter === 'q' ) $letter = 'Qu'; | |
| if ( $letter === '.' ) $letter = ''; | |
| if ( $letter === '*' ) $wild_card = true; | |
| $table[ $row_counter ][ $cell_counter ] = $letter; | |
| $cell_counter++; | |
| } | |
| $row_counter++; | |
| } | |
| global $highest_score; | |
| $highest_score = 0; | |
| global $words; | |
| $untrimmed_words = file( 'words_alpha.txt' ); | |
| foreach ( $untrimmed_words as $word ) { | |
| $words[ trim( $word ) ] = true; | |
| } | |
| unset( $untrimmed_words ); | |
| echo "Trimmed the word list.\n"; | |
| global $three_letters; | |
| foreach ( array_keys( $words ) as $word ) { | |
| $three_letters[ substr( $word, 0, 3 ) ] = true; | |
| } | |
| echo "Generated three-letter words.\n"; | |
| global $four_letters; | |
| foreach ( array_keys( $words ) as $word ) { | |
| $four_letters[ substr( $word, 0, 4 ) ] = true; | |
| } | |
| echo "Generated four-letter words.\n"; | |
| global $five_letters; | |
| foreach ( array_keys( $words ) as $word ) { | |
| $five_letters[ substr( $word, 0, 5 ) ] = true; | |
| } | |
| echo "Generated five-letter words.\n"; | |
| global $six_letters; | |
| foreach ( array_keys( $words ) as $word ) { | |
| $six_letters[ substr( $word, 0, 6 ) ] = true; | |
| } | |
| echo "Generated six-letter words.\n"; | |
| global $seven_letters; | |
| foreach ( array_keys( $words ) as $word ) { | |
| $seven_letters[ substr( $word, 0, 7 ) ] = true; | |
| } | |
| echo "Generated seven-letter words.\n"; | |
| global $eight_letters; | |
| foreach ( array_keys( $words ) as $word ) { | |
| $eight_letters[ substr( $word, 0, 8 ) ] = true; | |
| } | |
| echo "Generated eight-letter words.\n"; | |
| global $nine_letters; | |
| foreach ( array_keys( $words ) as $word ) { | |
| $nine_letters[ substr( $word, 0, 9 ) ] = true; | |
| } | |
| echo "Generated nine-letter words.\n"; | |
| echo "\n"; | |
| global $point_values; | |
| $point_values = array( | |
| 'a' => 1, | |
| 'b' => 2, | |
| 'c' => 2, | |
| 'd' => 2, | |
| 'e' => 1, | |
| 'f' => 3, | |
| 'g' => 2, | |
| 'h' => 3, | |
| 'i' => 1, | |
| 'j' => 4, | |
| 'k' => 4, | |
| 'l' => 1, | |
| 'm' => 2, | |
| 'n' => 1, | |
| 'o' => 1, | |
| 'p' => 2, | |
| 'q' => 4, | |
| 'r' => 1, | |
| 's' => 1, | |
| 't' => 1, | |
| 'u' => 1, | |
| 'v' => 3, | |
| 'w' => 3, | |
| 'x' => 4, | |
| 'y' => 3, | |
| 'z' => 5, | |
| '*' => 0, | |
| ); | |
| global $words_found; | |
| $words_found = array(); | |
| generate_words( $table, $words_found, false ); | |
| usort( $words_found, 'word_cmp' ); | |
| $original_words_found = $words_found; | |
| $original_table = $table; | |
| $original_bag = $bag; | |
| $best_opening_move = ''; | |
| $best_opening_score = -99999999; | |
| $i = 0; | |
| foreach ( $original_words_found as $best_word ) { | |
| $table = $original_table; | |
| $bag = $original_bag; | |
| $opening_coords = $best_word['coords']; | |
| fill_the_board( $table, $bag, $best_word['coords'] ); | |
| $continue = true; | |
| $first_go = true; | |
| $my_total_score = $original_my_score; | |
| $their_total_score = $original_their_score; | |
| $words_found = $original_words_found; | |
| $it_is_my_turn = $original_turn; | |
| do { | |
| if ( ! $first_go ) { | |
| generate_words( $table, $words_found, false ); | |
| usort( $words_found, 'word_cmp' ); | |
| if ( ! empty( $words_found ) ) { | |
| $best_word = array_shift( $words_found ); | |
| } | |
| fill_the_board( $table, $bag, $best_word['coords'] ); | |
| } | |
| if ( ! empty ( $words_found ) ) { | |
| if ( $it_is_my_turn ) { | |
| $my_total_score += $best_word['score']; | |
| $coords = replace_coords( $best_word['coords'] ); | |
| echo 'I play ' . $best_word['word'] . ' (' . $coords . ') for ' . $best_word['score'] . ' points (total ' . $my_total_score . ")\n"; | |
| $it_is_my_turn = false; | |
| } else { | |
| $their_total_score += $best_word['score']; | |
| $coords = replace_coords( $best_word['coords'] ); | |
| echo 'They play ' . $best_word['word'] . ' (' . $coords . ') for ' . $best_word['score'] . ' points (total ' . $their_total_score . ")\n"; | |
| $it_is_my_turn = true; | |
| } | |
| $words_found = array(); | |
| $first_go = false; | |
| } else { | |
| $continue = false; | |
| $total_score = $my_total_score - $their_total_score; | |
| if ( $total_score > $best_opening_score ) { | |
| $best_opening_move = replace_coords( $opening_coords ); | |
| $best_opening_score = $total_score; | |
| } | |
| echo "\nFINAL RESULT: $total_score – ME $my_total_score, THEY $their_total_score - BEST MOVE SO FAR: $best_opening_move\n\n"; | |
| $first_go = true; | |
| } | |
| } while ( $continue ); | |
| } | |
| function print_table( $table ) { | |
| for ( $x = 0; $x < 5; $x++ ) { | |
| for ( $y = 0; $y < 5; $y++ ) { | |
| echo $table[ $x ][ $y ]; | |
| } | |
| echo "\n"; | |
| } | |
| } | |
| function replace_coords( $coords ) { | |
| return str_replace( array( '0', '1', '2', '3', '4' ), array( 'a', 'b', 'c', 'd', 'e' ), $coords ); | |
| } | |
| /* | |
| generate_words( $table, $words_found, true ); | |
| usort( $words_found, 'word_cmp' ); | |
| $total_words = array(); | |
| $best_result = -99999; | |
| $counter = 20; | |
| foreach ( $words_found as $word ) { | |
| $counter--; | |
| if ( $counter < 1 ) break; | |
| $myword = $word['word']; | |
| $score = $word['score']; | |
| $coords = $word['coords']; | |
| if ( $score < $best_result ) { | |
| echo "No point looking at $myword.\n"; | |
| continue; | |
| } | |
| $new_table = $table; | |
| fill_the_board( $new_table, $bag, $word['coords'] ); | |
| $next_move_words_found = array(); | |
| echo "Generating next move words for $myword... "; | |
| $debug = false; | |
| generate_words( $new_table, $next_move_words_found, $debug ); | |
| echo "done, $counter to go.\n"; | |
| usort( $next_move_words_found, 'word_cmp' ); | |
| $word = array_shift( $next_move_words_found ); | |
| $their_word = $word['word']; | |
| $their_score = $word['score']; | |
| $total_score = $score - $their_score; | |
| $coords = str_replace( array( '0', '1', '2', '3', '4' ), array( 'a', 'b', 'c', 'd', 'e' ), $coords ); | |
| $text = "$myword ($coords) – my score: $score – opponent word: $their_word - total score: $total_score \n"; | |
| if ( $total_score > $best_result ) { | |
| echo $text; | |
| $best_result = $total_score; | |
| } | |
| $total_words[] = array( | |
| 'score' => $total_score, | |
| 'text' => $text, | |
| ); | |
| } | |
| usort( $total_words, 'word_cmp' ); | |
| echo "\nBest results:\n"; | |
| $counter = 5; | |
| while ( $counter > 0 && ! empty( $total_words ) ) { | |
| $row = array_shift( $total_words ); | |
| echo $row['text']; | |
| $counter--; | |
| } | |
| */ | |
| $end_time = microtime( true ); | |
| $time = round( $end_time - $begin_time, 2 ); | |
| echo "\nTime spent: $time s\n"; | |
| function fill_the_board( &$board, &$bag, $moves ) { | |
| $bag_letters = explode( ' ', strtolower( $bag ) ); | |
| $moves = str_split( $moves, 2 ); | |
| do { | |
| $move = array_shift( $moves ); | |
| $x = substr( $move, 0, 1 ); | |
| $y = substr( $move, 1, 1 ); | |
| if ( ! empty( $bag_letters ) ) { | |
| $letter = array_shift( $bag_letters ); | |
| $letter = substr( $letter, 0, 1 ); | |
| if ( $letter === 'Q' ) $letter = 'Qu'; | |
| $board[ $x ][ $y ] = $letter; | |
| } else { | |
| $board[ $x ][ $y ] = ''; | |
| } | |
| } while ( ! empty( $moves ) ); | |
| $bag = implode( ' ', $bag_letters ); | |
| } | |
| function word_cmp( $a, $b ) { | |
| return $a['score'] < $b['score']; | |
| } | |
| function score_word( $word ) { | |
| global $point_values; | |
| $score = 0; | |
| $multiplier = 1; | |
| for ( $i = 0; $i < strlen( $word ); $i++ ) { | |
| $letter = substr( $word, $i, 1 ); | |
| if ( $letter == '2' ) { | |
| $multiplier *= 2; | |
| } elseif ( $letter == '3' ) { | |
| $multiplier *= 3; | |
| } else { | |
| $score += $point_values[ $letter ]; | |
| } | |
| } | |
| $word = str_replace( array( '2', '3' ), '', $word ); | |
| $word = str_replace( 'qu', 'q', $word ); | |
| if ( strlen( $word ) >= 7 ) { | |
| $multiplier *= 2; | |
| } | |
| if ( strlen( $word ) < 3 ) { | |
| $multiplier = 1; | |
| } | |
| $score *= $multiplier; | |
| return $score; | |
| } | |
| function generate_words( $data, &$words_found, $verbose = false ) { | |
| global $coordinates_checked; | |
| $coordinates_checked = array(); | |
| $counter = 1; | |
| for ( $r = 0; $r < 5; $r++ ) { | |
| for ( $c = 0; $c < 5; $c++ ) { | |
| if ( $verbose ) echo "Working on square $counter\n"; | |
| $counter++; | |
| $letter = get_letter( $r, $c, $data ); | |
| if ( ! empty( $letter ) ) { | |
| $coords = $r . $c; | |
| traverse_board( $r, $c, $data, 1, $letter, $coords, $words_found, $verbose ); | |
| } | |
| } | |
| } | |
| } | |
| function get_letter( $r, $c, $data ) { | |
| $letter = $data[ $r ][ $c ]; | |
| if ( ! empty( $letter ) ) { | |
| if ( $r == 0 && $c == 0 ) $letter .= '2'; | |
| if ( $r == 0 && $c == 4 ) $letter .= '3'; | |
| if ( $r == 4 && $c == 0 ) $letter .= '3'; | |
| if ( $r == 4 && $c == 4 ) $letter .= '2'; | |
| } | |
| return $letter; | |
| } | |
| function traverse_board( $r, $c, $data, $depth, $word, $coordinates, &$words_found, $verbose = true ) { | |
| global $words, $three_letters, $four_letters, $five_letters, $six_letters, $seven_letters, $eight_letters, $nine_letters, $highest_score, $coordinates_checked; | |
| $data[ $r ][ $c ] = '-'; | |
| $test_word = str_replace( array( '2', '3' ), '', $word ); | |
| // If partial word doesn't appear in the list of existing prefixes, we can | |
| // stop going further down this rabbit hole. | |
| if ( strlen( $test_word ) == 3 && ! test_word( $test_word, $three_letters, 3 ) ) return; | |
| if ( strlen( $test_word ) == 4 && ! test_word( $test_word, $four_letters, 4 ) ) return; | |
| if ( strlen( $test_word ) == 5 && ! test_word( $test_word, $five_letters, 5 ) ) return; | |
| if ( strlen( $test_word ) == 6 && ! test_word( $test_word, $six_letters, 6 ) ) return; | |
| if ( strlen( $test_word ) == 7 && ! test_word( $test_word, $seven_letters, 7 ) ) return; | |
| if ( strlen( $test_word ) == 8 && ! test_word( $test_word, $eight_letters, 8 ) ) return; | |
| if ( strlen( $test_word ) == 9 && ! test_word( $test_word, $nine_letters, 9 ) ) return; | |
| if ( $depth > 1 ) { | |
| if ( ! isset( $coordinates_checked[ $coordinates ] ) ) { | |
| $coordinates_checked[ $coordinates ] = true; | |
| } | |
| if ( test_word( $test_word, $words ) ) { | |
| $score = score_word( $word ); | |
| $words_found[] = array( | |
| 'word' => $word, | |
| 'score' => $score, | |
| 'coords' => $coordinates, | |
| ); | |
| if ( $score > $highest_score ) { | |
| $highest_score = $score; | |
| if ( $verbose ) { | |
| echo "$word ($score)\n"; | |
| } | |
| } | |
| } | |
| } | |
| $neighbours = get_neighbours( $r, $c, $data ); | |
| if ( empty ( $neighbours ) ) return; | |
| foreach ( $neighbours as $coords ) { | |
| $coords = explode( '+', $coords ); | |
| $new_letter = get_letter( $coords[0], $coords[1], $data ); | |
| $new_word = $word . $new_letter; | |
| $new_coordinates = $coordinates . $coords[0] . $coords[1]; | |
| if ( isset( $coordinates_checked[ $new_coordinates ] ) ) continue; | |
| traverse_board( $coords[0], $coords[1], $data, $depth + 1, $new_word, $new_coordinates, $words_found, $verbose ); | |
| } | |
| return; | |
| } | |
| function test_word( $word, $list ) { | |
| if ( strpos( $word, '*' ) !== false ) { | |
| $pattern = '/^' . str_replace( '*', '.+', $word ) . '$/'; | |
| global $words; | |
| $matches = preg_grep( $pattern, array_keys( $words ) ); | |
| if ( count( $matches ) > 0 ) { | |
| return true; | |
| } | |
| } else { | |
| return isset( $list[ $word ] ); | |
| } | |
| return false; | |
| } | |
| function get_neighbours( $r, $c, $data ) { | |
| $neighbours = array(); | |
| for ( $rx = $r-1; $rx <= $r+1; $rx++ ) { | |
| for ( $cy = $c-1; $cy <= $c+1; $cy++ ) { | |
| if ( $rx == $r && $cy == $c ) continue; | |
| if ( $rx < 0 || $rx > 4 || $cy < 0 || $cy > 4 ) continue; | |
| $coordinates = $r . $c . $rx . $cy; | |
| $forbidden_moves = array( | |
| '0011', '1100', | |
| '4031', '3140', | |
| '0110', '1001', | |
| '0314', '1403', | |
| '3041', '4130', | |
| '3443', '4334', | |
| '0413', '1304', | |
| '4433', '3344', | |
| ); | |
| if ( in_array( $coordinates, $forbidden_moves ) ) continue; | |
| if ( $data[ $rx ][ $cy ] == '-' ) continue; | |
| if ( $data[ $rx ][ $cy ] ) { | |
| $neighbours[] = $rx . '+' . $cy; | |
| } else { | |
| $new_neighbours = get_neighbours_simple( $rx, $cy, $data ); | |
| $checked_these = array(); | |
| do { | |
| $count_neighbours = count( $new_neighbours ); | |
| $temp_neighbours = $new_neighbours; | |
| foreach ( $new_neighbours as $coords ) { | |
| if ( isset( $checked_these[ $coords ] ) ) { | |
| continue; | |
| } | |
| list( $x, $y ) = explode( '+', $coords ); | |
| $checked_these[ $coords ] = true; | |
| if ( empty( $data[ $x ][ $y ] ) ) { | |
| $more_neighbours = get_neighbours_simple( $x, $y, $data ); | |
| $temp_neighbours = array_merge( $temp_neighbours, $more_neighbours ); | |
| } | |
| } | |
| $new_neighbours = array_unique( $temp_neighbours ); | |
| } while ( count($new_neighbours ) > $count_neighbours ); | |
| $non_empty_neighbours = array(); | |
| foreach ( $new_neighbours as $coords ) { | |
| list( $x, $y ) = explode( '+', $coords ); | |
| if ( ! empty( $data[ $x ][ $y ] ) ) { | |
| $non_empty_neighbours[] = $coords; | |
| } | |
| } | |
| $neighbours = array_merge( $neighbours, $non_empty_neighbours ); | |
| } | |
| } | |
| } | |
| return $neighbours; | |
| } | |
| function get_neighbours_simple( $x, $y, $data ) { | |
| $neighbours = array(); | |
| for ( $xx = $x - 1; $xx <= $x + 1; $xx++ ) { | |
| for ( $yy = $y - 1; $yy <= $y + 1; $yy++ ) { | |
| if ( $xx == $x && $yy == $y ) continue; | |
| if ( $xx < 0 || $xx > 4 || $yy < 0 || $yy > 4 ) continue; | |
| $coordinates = $x . $y . $xx . $yy; | |
| $forbidden_moves = array( | |
| '0011', '1100', | |
| '4031', '3140', | |
| '0110', '1001', | |
| '0314', '1403', | |
| '3041', '4130', | |
| '3443', '4334', | |
| '0413', '1304', | |
| '4433', '3344', | |
| ); | |
| if ( in_array( $coordinates, $forbidden_moves ) ) continue; | |
| if ( $data[ $xx ][ $yy ] !== '-' ) { | |
| $neighbours[] = $xx . '+' . $yy; | |
| } | |
| } | |
| } | |
| return $neighbours; | |
| } | |
| /** | |
| * Generates all letter combinations in the word list up to five letters and | |
| * returns them in the order of descending commonness. | |
| */ | |
| function generate_letter_combos( ) { | |
| global $words; | |
| foreach ( array_keys( $words ) as $word ) { | |
| for ( $i = 0; $i < strlen( $word ); $i++ ) { | |
| $remaining_letters = strlen( substr( $word, $i ) ); | |
| $singles = trim( substr( $word, $i, 1 ) ); | |
| if ( ! isset( $letter_combos[ $singles ] ) ) $letter_combos[ $singles ] = 0; | |
| $letter_combos[ $singles ]++; | |
| if ( $remaining_letters < 2 ) continue; | |
| $pair = trim( substr( $word, $i, 2 ) ); | |
| if ( ! isset( $letter_combos[ $pair ] ) ) $letter_combos[ $pair ] = 0; | |
| $letter_combos[ $pair ]++; | |
| if ( $remaining_letters < 3 ) continue; | |
| $triplet = trim( substr( $word, $i, 3 ) ); | |
| if ( ! isset( $letter_combos[ $triplet ] ) ) $letter_combos[ $triplet ] = 0; | |
| $letter_combos[ $triplet ]++; | |
| if ( $remaining_letters < 4 ) continue; | |
| $four_letter = trim( substr( $word, $i, 4 ) ); | |
| if ( ! isset( $letter_combos[ $four_letter ] ) ) $letter_combos[ $four_letter ] = 0; | |
| $letter_combos[ $four_letter ]++; | |
| if ( $remaining_letters < 5 ) continue; | |
| $five_letter = trim( substr( $word, $i, 5 ) ); | |
| if ( ! isset( $letter_combos[ $five_letter ] ) ) $letter_combos[ $five_letter ] = 0; | |
| $letter_combos[ $five_letter ]++; | |
| } | |
| } | |
| arsort( $letter_combos ); | |
| $letter_combos = array_keys( $letter_combos ); | |
| return $letter_combos; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment