/app/views/blogs/

go-randomizer' title='lingo-randomizer Git repository'/>
summary refs log blame commit diff stats
path: root/generator.cpp
blob: c4dd76f314739b0759de0f69fb8741b91d153f1a (plain) (tree)
1
2
3
4
5
6


                      


                          




































































































































































































































































                                                                                                                                                  




                                                                      



















                                                                                                                     





                                                                                                               










                                                                                                                                                                        


                                                                                                                                              




















                                                                                                                                      




                                                                      























                                                                                                                                 





                                                                                                                        































                                                                                                                                                                  





                                                                                                                        





































                                                                                                                                                                        





                                                                                                                   


















































































































































































                                                                                                                                                                                                        























                                                       

















                                                                                                                    
                                                                         













































                                                                                                   



                                       
















                                                                                                                         


































                                                                                                             


               
#include "generator.h"
#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <cctype>
#include <hkutil/string.h>

verbly::filter Generator::MakeHintFilter(verbly::filter subfilter, Height height, Colour colour, FilterDirection filter_direction)
{
  switch (colour) {
    case kWhite: {
      switch (height) {
        case kBottom: {
          return (verbly::word::synonyms %= subfilter);
        }
        case kTop: {
          return (verbly::form::pronunciations %=
            verbly::filter("homophones", false,
              (verbly::pronunciation::forms %= (subfilter && verbly::filter(
                verbly::form::id,
                verbly::filter::comparison::field_does_not_equal,
                verbly::form::id)))));
        }
        case kMiddle: {
          return subfilter;
        }
        default: break; // Not supported yet.
      }
      break;
    }
    case kBlack: {
      switch (height) {
        case kBottom: {
          return (verbly::word::antonyms %= subfilter);
        }
        case kMiddle: {
          return (verbly::form::antogram %= subfilter);
        }
        case kTop: {
          return (verbly::pronunciation::antophone %= subfilter);
        }
        default: break; // Not supported yet.
      }
      break;
    }
    case kBrown: {
      break; // Not supported yet.
    }
    case kRed: {
      switch (height) {
        case kTop: {
          if (filter_direction == kTowardSolution)
          {
            return (verbly::pronunciation::merophones %= subfilter);
          } else {
            return (verbly::pronunciation::holophones %= subfilter);
          }
        }
        case kMiddle: {
          if (filter_direction == kTowardSolution)
          {
            return (verbly::form::merographs %= subfilter);
          } else {
            return (verbly::form::holographs %= subfilter);
          }
        }
        case kBottom: {
          if (filter_direction == kTowardSolution)
          {
            return (verbly::notion::partMeronyms %=
              verbly::filter("partMeronyms", false,
                subfilter && verbly::filter(
                  verbly::form::id,
                  verbly::filter::comparison::field_does_not_equal,
                  verbly::form::id)));
          } else {
            return (verbly::notion::partHolonyms %=
              verbly::filter("partHolonyms", false,
                subfilter && verbly::filter(
                  verbly::form::id,
                  verbly::filter::comparison::field_does_not_equal,
                  verbly::form::id)));
          }
        }
        default: break; // Not supported yet.
      }
      break;
    }
    case kBlue: {
      switch (height) {
        case kTop: {
          if (filter_direction == kTowardSolution)
          {
            return (verbly::pronunciation::holophones %= subfilter);
          } else {
            return (verbly::pronunciation::merophones %= subfilter);
          }
        }
        case kMiddle: {
          if (filter_direction == kTowardSolution)
          {
            return (verbly::form::holographs %= subfilter);
          } else {
            return (verbly::form::merographs %= subfilter);
          }
        }
        case kBottom: {
          if (filter_direction == kTowardSolution)
          {
            return (verbly::notion::partHolonyms %=
              verbly::filter("partHolonyms", false,
                subfilter && verbly::filter(
                  verbly::form::id,
                  verbly::filter::comparison::field_does_not_equal,
                  verbly::form::id)));
          } else {
            return (verbly::notion::partMeronyms %=
              verbly::filter("partMeronyms", false,
                subfilter && verbly::filter(
                  verbly::form::id,
                  verbly::filter::comparison::field_does_not_equal,
                  verbly::form::id)));
          }
        }
        default: break; // Not supported yet.
      }
      break;
    }
    case kPurple: {
      switch (height) {
        case kMiddle: {
          return (verbly::form::holographs %=
            verbly::filter("midpurp", false,
              (verbly::form::length >= 4 && (verbly::form::merographs %=
                (subfilter && verbly::filter(
                  verbly::form::id,
                  verbly::filter::comparison::field_does_not_equal,
                  verbly::form::id))))));
        }
        case kTop: {
          return (verbly::pronunciation::rhymes %= subfilter);
        }
        default: break; // Not supported yet.
      }
      break;
    }
    case kYellow: {
      switch (height) {
        case kTop: {
          return (verbly::pronunciation::anaphones %= (subfilter && verbly::filter(
                verbly::pronunciation::id,
                verbly::filter::comparison::field_does_not_equal,
                verbly::pronunciation::id)));
        }
        case kMiddle: {
          return (verbly::form::anagrams %= (subfilter && verbly::filter(
                verbly::form::id,
                verbly::filter::comparison::field_does_not_equal,
                verbly::form::id)));
        }
        default: break; // Not supported yet.
      }
      break;
    }
    case kGreen: {
      if (filter_direction == kTowardSolution)
      {
        switch (height) {
          case kBottom: {
            verbly::filter whitelist =
              (verbly::notion::wnid == 109287968)    // Geological formations
              || (verbly::notion::wnid == 109208496) // Asterisms (collections of stars)
              || (verbly::notion::wnid == 109239740) // Celestial bodies
              || (verbly::notion::wnid == 109277686) // Exterrestrial objects (comets and meteroids)
              || (verbly::notion::wnid == 109403211) // Radiators (supposedly natural radiators but actually these are just pictures of radiators)
              || (verbly::notion::wnid == 109416076) // Rocks
              || (verbly::notion::wnid == 105442131) // Chromosomes
              || (verbly::notion::wnid == 100324978) // Tightrope walking
              || (verbly::notion::wnid == 100326094) // Rock climbing
              || (verbly::notion::wnid == 100433458) // Contact sports
              || (verbly::notion::wnid == 100433802) // Gymnastics
              || (verbly::notion::wnid == 100439826) // Track and field
              || (verbly::notion::wnid == 100440747) // Skiing
              || (verbly::notion::wnid == 100441824) // Water sport
              || (verbly::notion::wnid == 100445351) // Rowing
              || (verbly::notion::wnid == 100446980) // Archery
                // TODO: add more sports
              || (verbly::notion::wnid == 100021939) // Artifacts
              || (verbly::notion::wnid == 101471682) // Vertebrates
                ;

            verbly::filter blacklist =
              (verbly::notion::wnid == 106883725) // swastika
              || (verbly::notion::wnid == 104416901) // tetraskele
              || (verbly::notion::wnid == 102512053) // fish
              || (verbly::notion::wnid == 103575691) // instrument of execution
              || (verbly::notion::wnid == 103829563) // noose
              || (verbly::notion::wnid == 103663910) // life support
                ;

            return subfilter
              && (verbly::notion::fullHypernyms %= whitelist)
              && !(verbly::notion::fullHypernyms %= blacklist)
              && (verbly::notion::partOfSpeech == verbly::part_of_speech::noun)
              && (verbly::notion::numOfImages >= 1);
          }
          case kMiddle: {
            return subfilter;
          }
          default: break; // Never supported.
        }
      } else {
        return (verbly::form::text == "picture");
      }
      break;
    }
    default: break; // Not supported yet.
  }
  return {};
}

std::string ApplyWanderlust(const std::string& word) {
  std::string result;
  for (char ch : word) {
    if (ch == 'w') {
      result += '1';
    } else if (ch == 'a') {
      result += '2';
    } else if (ch == 'n') {
      result += '3';
    } else if (ch == 'd') {
      result += '4';
    } else if (ch == 'e') {
      result += '5';
    } else if (ch == 'r') {
      result += '6';
    } else if (ch == 'l') {
      result += '7';
    } else if (ch == 'u') {
      result += '8';
    } else if (ch == 's') {
      result += '9';
    } else if (ch == 't') {
      result += '0';
    } else if (ch == ' ') {
      result += ' ';
    }
  }
  return result;
}

void Generator::GenerateStaticPanel(std::string name, std::string question, std::string answer) {
  SavePanel(name, question, answer.empty() ? question : answer, {});
}

void Generator::GenerateSinglePanel(std::string name, Height height, Colour colour, GenerateOptions options) {
  while (!GenerateSinglePanelImpl(name, height, colour, options));
}

bool Generator::GenerateSinglePanelImpl(std::string name, Height height, Colour colour, GenerateOptions options) {
  verbly::form solution;
  if (options.reuse_solution) {
    const std::string& word = reusable_.at(std::uniform_int_distribution<int>(0, reusable_.size()-1)(rng_));
    if (options.max_answer_len > 0 && word.size() > options.max_answer_len) return false;
    solution = database_->forms(verbly::form::text == word).first();
  } else {
    verbly::filter forward = MakeHintFilter({}, height, colour, kTowardSolution);
    if (options.must_be_broad) {
      forward &= MakeHintFilter({}, kBottom, kWhite, kTowardSolution);
      forward &= MakeHintFilter({}, kBottom, kRed, kTowardSolution);
      forward &= MakeHintFilter({}, kBottom, kBlue, kTowardSolution);
    }
    std::vector<verbly::form> solutions = database_->forms(forward && GetWordFilter(kTowardSolution, options)).all();
    solution = solutions.front();//solutions.at(std::uniform_int_distribution<int>(0, solutions.size())(rng_));
  }

  if (!options.unique_pool.empty() && pools_[options.unique_pool].count(solution.getText())) {
    return false;
  }
  if (options.palindrome != kPalindromeUnspecified) {
    std::string reversed = solution.getText();
    std::reverse(reversed.begin(), reversed.end());

    if ((options.palindrome == kForcePalindrome && reversed != solution.getText()) ||
      (options.palindrome == kRejectPalindrome && reversed == solution.getText())) {
      return false;
    }
  }

  // Finish early if this is a middle white.
  if (height == kMiddle && colour == kWhite) {
    SavePanel(name, solution.getText(), solution.getText(), options);
    if (!options.copy_to.empty()) {
      SavePanel(options.copy_to, solution.getText(), solution.getText(), options);
    }
    if (!options.copy_hidden.empty()) {
      SavePanel(options.copy_hidden, std::string(solution.getText().size(), '?'), solution.getText(), options);
    }
    return true;
  }

  verbly::filter questionFilter = MakeHintFilter(solution, height, colour, kTowardQuestion);
  std::vector<verbly::form> questions = database_->forms(questionFilter && GetWordFilter(kTowardQuestion, options)).all();//, verbly::order(verbly::form::id), 0).all();
  if (questions.size() < 1) return false;
  verbly::form question = questions.front();// questions.at(std::uniform_int_distribution<int>(0, questions.size())(rng_));

  if (IsClueTrivial(height, colour, question, solution)) {
    return false;
  }
  if (options.max_len_diff >= 0 && std::abs(static_cast<int>(question.getText().size() - solution.getText().size())) > options.max_len_diff) {
    return false;
  }

  SavePanel(name, question.getText(), solution.getText(), options);
  if (!options.copy_to.empty()) {
    SavePanel(options.copy_to, question.getText(), solution.getText(), options);
  }

  return true;
}

void Generator::GenerateDoublePanel(std::string name1, std::string name2, Height height, Colour colour, GenerateOptions options) {
  while (!GenerateDoublePanelImpl(name1, name2, height, colour, options));
}

bool Generator::GenerateDoublePanelImpl(std::string name1, std::string name2, Height height, Colour colour, GenerateOptions options) {
  verbly::form solution;
  if (options.reuse_solution) {
    const std::string& word = reusable_.at(std::uniform_int_distribution<int>(0, reusable_.size()-1)(rng_));
    if (options.max_answer_len > 0 && word.size() > options.max_answer_len) return false;
    solution = database_->forms(verbly::form::text == word).first();
  } else {
    verbly::filter forward = MakeHintFilter({}, height, colour, kTowardSolution);
    if (options.must_be_broad) {
      forward &= MakeHintFilter({}, kBottom, kWhite, kTowardSolution);
      forward &= MakeHintFilter({}, kBottom, kRed, kTowardSolution);
      forward &= MakeHintFilter({}, kBottom, kBlue, kTowardSolution);
    }
    std::vector<verbly::form> solutions = database_->forms(forward && GetWordFilter(kTowardSolution, options)).all();
    solution = solutions.front();//solutions.at(std::uniform_int_distribution<int>(0, solutions.size())(rng_));
  }

  if (!options.unique_pool.empty() && pools_[options.unique_pool].count(solution.getText())) {
    return false;
  }

  // Finish early if this is a middle white.
  if (height == kMiddle && colour == kWhite) {
    SavePanel(name1, solution.getText(), solution.getText(), options);
    SavePanel(name2, solution.getText(), solution.getText(), options);
    return true;
  }

  verbly::filter questionFilter = MakeHintFilter(solution, height, colour, kTowardQuestion);
  std::vector<verbly::form> questions = database_->forms(questionFilter && GetWordFilter(kTowardQuestion, options), {}, 2).all();
  if (questions.size() < 2) return false;

  //std::shuffle(questions.begin(), questions.end(), rng_);

  if (IsClueTrivial(height, colour, questions[0], solution) || IsClueTrivial(height, colour, questions[1], solution)) {
    return false;
  }
  if (options.max_len_diff >= 0) {
    if (std::abs(static_cast<int>(questions[0].getText().size() - solution.getText().size())) > options.max_len_diff
      || std::abs(static_cast<int>(questions[1].getText().size() - solution.getText().size())) > options.max_len_diff) {
      return false;
    }
  }

  SavePanel(name1, questions[0].getText(), solution.getText(), options);
  SavePanel(name2, questions[1].getText(), solution.getText(), options);

  return true;
}

void Generator::GenerateCohintedPanels(std::string name1, std::string name2, Height height, Colour colour, GenerateOptions options) {
  while (!GenerateCohintedPanelsImpl(name1, name2, height, colour, options));
}

bool Generator::GenerateCohintedPanelsImpl(std::string name1, std::string name2, Height height, Colour colour, GenerateOptions options) {
  verbly::filter backward = MakeHintFilter({}, height, colour, kTowardQuestion);
  std::vector<verbly::form> questions = database_->forms(backward && GetWordFilter(kTowardQuestion, options)).all();//, verbly::order(verbly::form::id), 0).all();
  verbly::form question = questions.front();// solutions.at(std::uniform_int_distribution<int>(0, solutions.size())(rng_));

  // Finish early if this is a middle white.
  if (height == kMiddle && colour == kWhite) {
    SavePanel(name1, question.getText(), question.getText(), options);
    SavePanel(name2, question.getText(), question.getText(), options);
    return true;
  }

  verbly::filter solutionFilter = MakeHintFilter(question, height, colour, kTowardSolution);
  std::vector<verbly::form> solutions = database_->forms(solutionFilter && GetWordFilter(kTowardSolution, options), {}, 2).all();
  if (solutions.size() < 2) return false;

  //std::shuffle(questions.begin(), questions.end(), rng_);

  if (IsClueTrivial(height, colour, question, solutions[0]) || IsClueTrivial(height, colour, question, solutions[1])) {
    return false;
  }
  if (options.max_len_diff >= 0) {
    if (std::abs(static_cast<int>(question.getText().size() - solutions[0].getText().size())) > options.max_len_diff
      || std::abs(static_cast<int>(question.getText().size() - solutions[1].getText().size())) > options.max_len_diff) {
      return false;
    }
  }

  SavePanel(name1, question.getText(), solutions[0].getText(), options);
  SavePanel(name2, question.getText(), solutions[1].getText(), options);

  return true;
}

void Generator::GeneratePairedPanels(std::string name1, std::string name2, Height height, Colour colour, GenerateOptions options) {
  while (!GeneratePairedPanelsImpl(name1, name2, height, colour, options));
}

bool Generator::GeneratePairedPanelsImpl(std::string name1, std::string name2, Height height, Colour colour, GenerateOptions options) {
  Colour effectiveColour = (height == kMiddle && colour == kWhite) ? kBlack : colour;
  Height effectiveHeight = (height == kMiddle && colour == kWhite) ? kBottom : height;

  verbly::filter forward = MakeHintFilter({}, effectiveHeight, effectiveColour, kTowardSolution);
  std::vector<verbly::form> solutions = database_->forms(forward && GetWordFilter(kTowardSolution, options)).all();
  verbly::form solution = solutions.front();//solutions.at(std::uniform_int_distribution<int>(0, solutions.size())(rng_));

  verbly::filter questionFilter = MakeHintFilter(solution, effectiveHeight, effectiveColour, kTowardQuestion);
  std::vector<verbly::form> questions = database_->forms(questionFilter && GetWordFilter(kTowardQuestion, options)).all();//, verbly::order(verbly::form::id), 0).all();
  if (questions.size() < 1) return false;
  verbly::form question = questions.front();// questions.at(std::uniform_int_distribution<int>(0, questions.size())(rng_));

  if (IsClueTrivial(height, effectiveColour, question, solution)) {
    return false;
  }

  if (options.palindrome != kPalindromeUnspecified) {
    std::string reversed = question.getText();
    std::reverse(reversed.begin(), reversed.end());

    if ((options.palindrome == kForcePalindrome && reversed != question.getText()) ||
      (options.palindrome == kRejectPalindrome && reversed == question.getText())) {
      return false;
    }
  }

  if (options.max_len_diff >= 0) {
    if (std::abs(static_cast<int>(question.getText().size() - solution.getText().size())) > options.max_len_diff) {
      return false;
    }
  }

  if (height == kMiddle && colour == kWhite) {
    SavePanel(name1, question.getText(), question.getText(), options);
    SavePanel(name2, solution.getText(), solution.getText(), options);
    if (!options.copy_to.empty() && !options.copy_to2.empty()) {
      SavePanel(options.copy_to, question.getText(), question.getText(), options);
      SavePanel(options.copy_to2, solution.getText(), solution.getText(), options);
    }
  } else {
    SavePanel(name1, question.getText(), solution.getText(), options);
    SavePanel(name2, solution.getText(), question.getText(), options);
    if (!options.copy_to.empty() && !options.copy_to2.empty()) {
      SavePanel(options.copy_to, question.getText(), solution.getText(), options);
      SavePanel(options.copy_to2, solution.getText(), question.getText(), options);
    }
  }

  return true;
}

void Generator::GeneratePanelStack(std::string top_name, Colour top_colour, std::string middle_name, Colour middle_colour, std::string bottom_name, Colour bottom_colour, GenerateOptions options) {
  while (!GeneratePanelStackImpl(top_name, top_colour, middle_name, middle_colour, bottom_name, bottom_colour, options));
}

bool Generator::GeneratePanelStackImpl(std::string top_name, Colour top_colour, std::string middle_name, Colour middle_colour, std::string bottom_name, Colour bottom_colour, GenerateOptions options) {
  verbly::form solution;
  if (options.reuse_solution) {
    const std::string& word = reusable_.at(std::uniform_int_distribution<int>(0, reusable_.size()-1)(rng_));
    if (options.max_answer_len > 0 && word.size() > options.max_answer_len) return false;
    solution = database_->forms(verbly::form::text == word).first();
  } else {
    verbly::filter forward = GetWordFilter(kTowardSolution, options);
    if (!top_name.empty()) {
      forward &= MakeHintFilter({}, kTop, top_colour, kTowardSolution);
    }
    if (!middle_name.empty()) {
      forward &= MakeHintFilter({}, kMiddle, middle_colour, kTowardSolution);
    }
    if (!bottom_name.empty()) {
      forward &= MakeHintFilter({}, kBottom, bottom_colour, kTowardSolution);
    }
    std::vector<verbly::form> solutions = database_->forms(forward).all();
    solution = solutions.front();//solutions.at(std::uniform_int_distribution<int>(0, solutions.size())(rng_));
  }

  if (!options.unique_pool.empty() && pools_[options.unique_pool].count(solution.getText())) {
    return false;
  }

  std::string top_hint;
  std::string middle_hint;
  std::string bottom_hint;

  if (!top_name.empty()) {
    verbly::filter questionFilter = MakeHintFilter(solution, kTop, top_colour, kTowardQuestion);
    std::vector<verbly::form> questions = database_->forms(questionFilter && GetWordFilter(kTowardQuestion, options), {}, 1).all();
    if (questions.empty()) return false;
    top_hint = questions.front().getText();

    if (IsClueTrivial(kTop, top_colour, questions.front(), solution)) {
      return false;
    }
  }

  if (!middle_name.empty()) {
    verbly::filter questionFilter = MakeHintFilter(solution, kMiddle, middle_colour, kTowardQuestion);
    std::vector<verbly::form> questions = database_->forms(questionFilter && GetWordFilter(kTowardQuestion, options), {}, 1).all();
    if (questions.empty()) return false;
    middle_hint = questions.front().getText();

    if (IsClueTrivial(kMiddle, middle_colour, questions.front(), solution)) {
      return false;
    }
  }

  if (!bottom_name.empty()) {
    verbly::filter questionFilter = MakeHintFilter(solution, kBottom, bottom_colour, kTowardQuestion);
    std::vector<verbly::form> questions = database_->forms(questionFilter && GetWordFilter(kTowardQuestion, options), {}, 1).all();
    if (questions.empty()) return false;
    bottom_hint = questions.front().getText();

    if (IsClueTrivial(kBottom, bottom_colour, questions.front(), solution)) {
      return false;
    }
  }

  if (!top_name.empty()) {
    SavePanel(top_name, top_hint, solution.getText(), options);
  }
  if (!middle_name.empty()) {
    SavePanel(middle_name, middle_hint, solution.getText(), options);
  }
  if (!bottom_name.empty()) {
    SavePanel(bottom_name, bottom_hint, solution.getText(), options);
  }

  return true;
}

void Generator::GenerateOrangeNumberPanel(std::string name) {
  std::string solution = wanderlust_->GetWord(rng_);
  std::string question = ApplyWanderlust(solution);

  SavePanel(name, question, solution);
}

void Generator::GenerateOrangeWordPanel(std::string name) {
  std::string question = wanderlust_->GetWord(rng_);
  std::string solution = ApplyWanderlust(question);

  SavePanel(name, question, solution);
}

void Generator::GenerateOrangeAdditionPanel(std::string name) {
  auto [question, solution] = wanderlust_->GetPuzzle(rng_);

  SavePanel(name, question, solution);
}

void Generator::GenerateOneRoadManyTurns(std::string order_name, std::string part1_name, std::string part2_name, std::string part3_name, std::string part4_name) {
  const auto& [part1_q, part1_a] = panels_.at(part1_name);
  const auto& [part2_q, part2_a] = panels_.at(part2_name);
  const auto& [part3_q, part3_a] = panels_.at(part3_name);
  const auto& [part4_q, part4_a] = panels_.at(part4_name);

  SavePanel(order_name, "order", part1_a + " " + part2_a + " " + part3_a + " " + part4_a);
}

void Generator::GenerateComboPanel(std::string name, Height left_height, Colour left_colour, Height right_height, Colour right_colour, GenerateOptions options) {
  while (!GenerateComboPanelImpl(name, left_height, left_colour, right_height, right_colour, options));
}

bool Generator::GenerateComboPanelImpl(std::string name, Height left_height, Colour left_colour, Height right_height, Colour right_colour, GenerateOptions options) {
  options.force_two_words = true;

  verbly::form solution = database_->forms(GetWordFilter(kTowardSolution, options)).first();
  std::string soltext = solution.getText();
  int spacepos = soltext.find(" ");
  std::string leftword = soltext.substr(0, spacepos);
  std::string rightword = soltext.substr(spacepos+1);

  options.force_two_words = false;
  verbly::filter left_filter = MakeHintFilter(verbly::form::text == leftword, left_height, left_colour, kTowardQuestion);
  std::vector<verbly::form> left_questions = database_->forms(left_filter && GetWordFilter(kTowardQuestion, options)).all();
  if (left_questions.size() < 1) return false;
  verbly::form left_question = left_questions.front();

  verbly::filter right_filter = MakeHintFilter(verbly::form::text == rightword, right_height, right_colour, kTowardQuestion);
  std::vector<verbly::form> right_questions = database_->forms(right_filter && GetWordFilter(kTowardQuestion, options)).all();
  if (right_questions.size() < 1) return false;
  verbly::form right_question = right_questions.front();

  SavePanel(name, left_question.getText() + " " + right_question.getText(), soltext, options);

  return true;
}

void Generator::GenerateCrossTower(
  std::string north_tower_name,
  std::string south_tower_name,
  std::string east_tower_name,
  std::string west_tower_name,
  std::string north_lookout_name,
  std::string south_lookout_name,
  std::string east_lookout_name,
  std::string west_lookout_name,
  std::string north_other_name1,
  std::string north_other_name2,
  std::string north_other_name3,
  std::string south_other_name1,
  std::string south_other_name2,
  std::string south_other_name3,
  std::string east_other_name1,
  std::string east_other_name2,
  std::string east_other_name3,
  std::string west_other_name1,
  std::string west_other_name2,
  std::string west_other_name3)
{
  std::vector<std::vector<std::string>> sets = cross_tower_->GetPuzzleSet(rng_);

  SavePanel(north_tower_name, "", sets[0][0]);
  SavePanel(north_lookout_name, "", sets[0][0]);
  SavePanel(north_other_name1, sets[0][1], sets[0][1]);
  SavePanel(north_other_name2, sets[0][2], sets[0][2]);
  SavePanel(north_other_name3, sets[0][3], sets[0][3]);

  SavePanel(south_tower_name, "", sets[1][0]);
  SavePanel(south_lookout_name, "", sets[1][0]);
  SavePanel(south_other_name1, sets[1][1], sets[1][1]);
  SavePanel(south_other_name2, sets[1][2], sets[1][2]);
  SavePanel(south_other_name3, sets[1][3], sets[1][3]);

  SavePanel(east_tower_name, "", sets[2][0]);
  SavePanel(east_lookout_name, "", sets[2][0]);
  SavePanel(east_other_name1, sets[2][1], sets[2][1]);
  SavePanel(east_other_name2, sets[2][2], sets[2][2]);
  SavePanel(east_other_name3, sets[2][3], sets[2][3]);

  SavePanel(west_tower_name, "", sets[3][0]);
  SavePanel(west_lookout_name, "", sets[3][0]);
  SavePanel(west_other_name1, sets[3][1], sets[3][1]);
  SavePanel(west_other_name2, sets[3][2], sets[3][2]);
  SavePanel(west_other_name3, sets[3][3], sets[3][3]);
}

void Generator::SavePanel(std::string name, std::string question, std::string answer, GenerateOptions options) {
  if (options.save_for_later) {
    reusable_.push_back(answer);
  }

  if (!options.unique_pool.empty()) {
    pools_[options.unique_pool].insert(answer);
  }

  if (options.obscure_hint) {
    int numToObscure = (question.size()/3 > 0) ? std::uniform_int_distribution<int>(1, question.size()/3)(rng_) : 1;
    std::vector<int> indicies(question.size());
    std::iota(indicies.begin(), indicies.end(), 0);
    std::shuffle(indicies.begin(), indicies.end(), rng_);

    for (int i=0; i<numToObscure; i++) {
      if (question[indicies[i]] != ' ' && question[indicies[i]] != '-') {
        question[indicies[i]] = '?';
      }
    }
  }

  panels_[name] = std::make_tuple(question, answer);

  std::cout << name << ": " << question << "? " << answer << "!" << std::endl;
}

verbly::filter Generator::GetWordFilter(FilterDirection dir, GenerateOptions options) const {
  verbly::filter wordFilter =
    (verbly::form::proper == false);

  wordFilter &= (
    !(verbly::word::usageDomains %= (verbly::notion::wnid == 106718862)) // ethnic slurs
    && !(verbly::notion::wnid == 110630093) // "spastic"
    && !(verbly::notion::fullHypernyms %= (verbly::notion::wnid == 100844254))); // sexual activity

  if (options.exact_len > 0) {
    wordFilter &= (verbly::form::length == options.exact_len);
  } else if (dir == kTowardSolution && options.max_answer_len > 0) {
    wordFilter &= (verbly::form::length <= options.max_answer_len);
  } else if (dir == kTowardQuestion && options.max_hint_len > 0) {
    wordFilter &= (verbly::form::length <= options.max_hint_len);
  } else {
    wordFilter &= (verbly::form::length <= 11);
  }

  if (options.exact_len == 0) {
    wordFilter &= (verbly::form::length >= 3);
  }

  if (!options.multiword) {
    if (options.force_two_words) {
      wordFilter &= (verbly::form::complexity == 2);
    } else {
      wordFilter &= (verbly::form::complexity == 1);
    }
  } else {
    wordFilter &= ((verbly::form::complexity > 1) || (verbly::form::frequency > 2000000));
  }

  return wordFilter;
}

bool isDigitWrapper(unsigned char ch) {
  return std::isdigit(ch);
}

bool Generator::IsClueTrivial(Height height, Colour colour, const verbly::form& clue, const verbly::form& solution) const
{
  if (height == kTop && colour == kWhite)
  {
    return !database_->forms((verbly::filter)clue && (verbly::word::synonyms %= solution)).all().empty();
  } else if (height == kBottom && colour == kWhite)
  {
    return !database_->forms((verbly::filter)clue && (verbly::form::pronunciations %= solution)).all().empty();
  } else if (height == kBottom && colour == kBlack)
  {
    return !database_->forms((verbly::filter)clue && (verbly::form::merographs %= solution)).all().empty()
      || !database_->forms((verbly::filter)clue && (verbly::form::holographs %= solution)).all().empty();
  } else if ((height == kMiddle || height == kTop) && colour == kPurple)
  {
    return (clue.getId() == solution.getId())
      || !database_->forms((verbly::filter)clue && (verbly::form::merographs %= solution)).all().empty()
      || !database_->forms((verbly::filter)clue && (verbly::form::holographs %= solution)).all().empty();
  } else if (height == kMiddle && colour == kRed) {
    if (clue.getComplexity() == 2 && solution.getComplexity() == 1) {
      auto words = hatkirby::split<std::vector<std::string>>(clue.getText(), " ");
      for (const auto& word : words) {
        if (word == solution.getText()) {
          return true;
        }
      }
    }
  } else if (height == kMiddle && colour == kYellow) {
    if (clue.getComplexity() == solution.getComplexity()) {
      auto clueWords = hatkirby::split<std::vector<std::string>>(clue.getText(), " ");
      auto solutionWords = hatkirby::split<std::vector<std::string>>(solution.getText(), " ");
      std::sort(clueWords.begin(), clueWords.end());
      std::sort(solutionWords.begin(), solutionWords.end());
      if (clueWords == solutionWords) {
        return true;
      }
    }
  } else if (height == kTop && colour == kYellow) {
    std::set<std::string> hint_stressless;
    for (const verbly::pronunciation& pronunciation : clue.getPronunciations()) {
      std::string stressed = hatkirby::implode(pronunciation.getPhonemes(), " ");
      std::string stressless;
      std::remove_copy_if(stressed.begin(), stressed.end(), std::back_inserter(stressless), &isDigitWrapper);
      hint_stressless.insert(stressless);
    }
    for (const verbly::pronunciation& pronunciation : solution.getPronunciations()) {
      std::string stressed = hatkirby::implode(pronunciation.getPhonemes(), " ");
      std::string stressless;
      std::remove_copy_if(stressed.begin(), stressed.end(), std::back_inserter(stressless), &isDigitWrapper);
      if (hint_stressless.count(stressless)) {
        return true;
      }
    }
  }
  return false;
}