diff options
| -rw-r--r-- | .gitmodules | 3 | ||||
| -rw-r--r-- | CMakeLists.txt | 6 | ||||
| -rw-r--r-- | imagenet.cpp | 89 | ||||
| -rw-r--r-- | imagenet.h | 22 | ||||
| -rw-r--r-- | lingo.cpp | 82 | ||||
| m--------- | vendor/curlcpp | 0 | ||||
| m--------- | vendor/verbly | 0 |
7 files changed, 197 insertions, 5 deletions
| diff --git a/.gitmodules b/.gitmodules index 40a5694..d502d11 100644 --- a/.gitmodules +++ b/.gitmodules | |||
| @@ -1,3 +1,6 @@ | |||
| 1 | [submodule "vendor/verbly"] | 1 | [submodule "vendor/verbly"] |
| 2 | path = vendor/verbly | 2 | path = vendor/verbly |
| 3 | url = https://github.com/hatkirby/verbly.git | 3 | url = https://github.com/hatkirby/verbly.git |
| 4 | [submodule "vendor/curlcpp"] | ||
| 5 | path = vendor/curlcpp | ||
| 6 | url = https://github.com/JosephP91/curlcpp | ||
| diff --git a/CMakeLists.txt b/CMakeLists.txt index 38883b5..56082ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
| @@ -7,19 +7,21 @@ find_package(PkgConfig) | |||
| 7 | pkg_check_modules(yaml-cpp yaml-cpp REQUIRED) | 7 | pkg_check_modules(yaml-cpp yaml-cpp REQUIRED) |
| 8 | pkg_check_modules(dpp dpp REQUIRED) | 8 | pkg_check_modules(dpp dpp REQUIRED) |
| 9 | 9 | ||
| 10 | add_subdirectory(vendor/curlcpp) | ||
| 10 | add_subdirectory(vendor/verbly) | 11 | add_subdirectory(vendor/verbly) |
| 11 | 12 | ||
| 12 | include_directories( | 13 | include_directories( |
| 13 | ${dpp_INCLUDE_DIRS} | 14 | ${dpp_INCLUDE_DIRS} |
| 14 | vendor/verbly/lib | 15 | vendor/verbly/lib |
| 15 | ${yaml-cpp_INCLUDE_DIRS} | 16 | ${yaml-cpp_INCLUDE_DIRS} |
| 17 | ${CURLCPP_SOURCE_DIR}/include | ||
| 16 | vendor/json) | 18 | vendor/json) |
| 17 | 19 | ||
| 18 | link_directories( | 20 | link_directories( |
| 19 | ${dpp_LIBRARY_DIRS} | 21 | ${dpp_LIBRARY_DIRS} |
| 20 | ${yaml-cpp_LIBRARY_DIRS}) | 22 | ${yaml-cpp_LIBRARY_DIRS}) |
| 21 | 23 | ||
| 22 | add_executable(lingo lingo.cpp) | 24 | add_executable(lingo lingo.cpp imagenet.cpp) |
| 23 | set_property(TARGET lingo PROPERTY CXX_STANDARD 17) | 25 | set_property(TARGET lingo PROPERTY CXX_STANDARD 17) |
| 24 | set_property(TARGET lingo PROPERTY CXX_STANDARD_REQUIRED ON) | 26 | set_property(TARGET lingo PROPERTY CXX_STANDARD_REQUIRED ON) |
| 25 | target_link_libraries(lingo verbly ${dpp_LIBRARIES} ${yaml-cpp_LIBRARIES}) | 27 | target_link_libraries(lingo verbly ${dpp_LIBRARIES} ${yaml-cpp_LIBRARIES} curlcpp curl) |
| diff --git a/imagenet.cpp b/imagenet.cpp new file mode 100644 index 0000000..3a107bd --- /dev/null +++ b/imagenet.cpp | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | #include "imagenet.h" | ||
| 2 | #include <fstream> | ||
| 3 | #include <stdexcept> | ||
| 4 | #include <sstream> | ||
| 5 | #include <vector> | ||
| 6 | #include <curl_easy.h> | ||
| 7 | #include <curl_header.h> | ||
| 8 | |||
| 9 | imagenet::imagenet(std::string path) : path_(path) {} | ||
| 10 | |||
| 11 | std::tuple<std::string, std::string> imagenet::getImageForNotion(int notion_id, std::mt19937& rng) const | ||
| 12 | { | ||
| 13 | std::filesystem::path filename = path_ / std::to_string(notion_id); | ||
| 14 | if (!std::filesystem::exists(filename)) | ||
| 15 | { | ||
| 16 | throw std::invalid_argument(std::string("File does not exist: ") + std::string(filename)); | ||
| 17 | } | ||
| 18 | |||
| 19 | std::ifstream file(filename); | ||
| 20 | std::string line; | ||
| 21 | std::vector<std::string> urls; | ||
| 22 | while (std::getline(file, line)) | ||
| 23 | { | ||
| 24 | if (!line.empty()) | ||
| 25 | { | ||
| 26 | urls.push_back(line); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | std::string output; | ||
| 31 | std::string extension; | ||
| 32 | while (!urls.empty()) | ||
| 33 | { | ||
| 34 | int index = std::uniform_int_distribution<int>(0, urls.size()-1)(rng); | ||
| 35 | std::string url = urls.at(index); | ||
| 36 | urls.erase(std::begin(urls) + index); | ||
| 37 | |||
| 38 | // willyfogg.com is a thumbnail generator known to return 200 even if the target image no longer exists | ||
| 39 | if (url.find("willyfogg.com/thumb.php") != std::string::npos) | ||
| 40 | { | ||
| 41 | continue; | ||
| 42 | } | ||
| 43 | |||
| 44 | // Accept string from Google Chrome | ||
| 45 | std::string accept = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"; | ||
| 46 | curl::curl_header headers; | ||
| 47 | headers.add(accept); | ||
| 48 | |||
| 49 | std::ostringstream imgbuf; | ||
| 50 | curl::curl_ios<std::ostringstream> imgios(imgbuf); | ||
| 51 | curl::curl_easy imghandle(imgios); | ||
| 52 | |||
| 53 | imghandle.add<CURLOPT_HTTPHEADER>(headers.get()); | ||
| 54 | imghandle.add<CURLOPT_URL>(url.c_str()); | ||
| 55 | imghandle.add<CURLOPT_CONNECTTIMEOUT>(30); | ||
| 56 | imghandle.add<CURLOPT_TIMEOUT>(300); | ||
| 57 | |||
| 58 | try | ||
| 59 | { | ||
| 60 | imghandle.perform(); | ||
| 61 | } catch (const curl::curl_easy_exception& error) { | ||
| 62 | error.print_traceback(); | ||
| 63 | |||
| 64 | continue; | ||
| 65 | } | ||
| 66 | |||
| 67 | if (imghandle.get_info<CURLINFO_RESPONSE_CODE>().get() != 200) | ||
| 68 | { | ||
| 69 | continue; | ||
| 70 | } | ||
| 71 | |||
| 72 | std::string content_type = imghandle.get_info<CURLINFO_CONTENT_TYPE>().get(); | ||
| 73 | if (content_type.substr(0, 6) != "image/") | ||
| 74 | { | ||
| 75 | continue; | ||
| 76 | } | ||
| 77 | |||
| 78 | output = imgbuf.str(); | ||
| 79 | extension = url.substr(url.rfind(".") + 1); | ||
| 80 | break; | ||
| 81 | } | ||
| 82 | |||
| 83 | if (output.empty()) | ||
| 84 | { | ||
| 85 | throw std::invalid_argument(std::string("No valid urls found for ") + std::string(filename)); | ||
| 86 | } | ||
| 87 | |||
| 88 | return {output, extension}; | ||
| 89 | } | ||
| diff --git a/imagenet.h b/imagenet.h new file mode 100644 index 0000000..e2c57de --- /dev/null +++ b/imagenet.h | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | #ifndef IMAGENET_H_41BE424F | ||
| 2 | #define IMAGENET_H_41BE424F | ||
| 3 | |||
| 4 | #include <string> | ||
| 5 | #include <filesystem> | ||
| 6 | #include <random> | ||
| 7 | #include <tuple> | ||
| 8 | |||
| 9 | class imagenet { | ||
| 10 | public: | ||
| 11 | |||
| 12 | explicit imagenet(std::string path); | ||
| 13 | |||
| 14 | // returns bytedata, extension | ||
| 15 | std::tuple<std::string, std::string> getImageForNotion(int notion_id, std::mt19937& rng) const; | ||
| 16 | |||
| 17 | private: | ||
| 18 | |||
| 19 | std::filesystem::path path_; | ||
| 20 | }; | ||
| 21 | |||
| 22 | #endif /* end of include guard: IMAGENET_H_41BE424F */ | ||
| diff --git a/lingo.cpp b/lingo.cpp index 439bab5..5f8a025 100644 --- a/lingo.cpp +++ b/lingo.cpp | |||
| @@ -13,6 +13,7 @@ | |||
| 13 | #include <optional> | 13 | #include <optional> |
| 14 | #include <map> | 14 | #include <map> |
| 15 | #include <array> | 15 | #include <array> |
| 16 | #include "imagenet.h" | ||
| 16 | 17 | ||
| 17 | #define ENABLE_BOT | 18 | #define ENABLE_BOT |
| 18 | 19 | ||
| @@ -31,6 +32,7 @@ enum Colour { | |||
| 31 | kPurple, | 32 | kPurple, |
| 32 | kBrown, | 33 | kBrown, |
| 33 | kYellow, | 34 | kYellow, |
| 35 | kGreen, | ||
| 34 | kColourCount | 36 | kColourCount |
| 35 | }; | 37 | }; |
| 36 | 38 | ||
| @@ -41,7 +43,8 @@ const std::string COLOUR_EMOJIS[kColourCount] = { | |||
| 41 | "🟦", | 43 | "🟦", |
| 42 | "🟪", | 44 | "🟪", |
| 43 | "🟫", | 45 | "🟫", |
| 44 | "🟨" | 46 | "🟨", |
| 47 | "🟩" | ||
| 45 | }; | 48 | }; |
| 46 | 49 | ||
| 47 | const std::string NONE_EMOTE = "<:xx:1047267830535557180>"; | 50 | const std::string NONE_EMOTE = "<:xx:1047267830535557180>"; |
| @@ -53,7 +56,8 @@ const std::string COLOUR_EMOTES[kColourCount] = { | |||
| 53 | "<:bl:1047262138202325042>", | 56 | "<:bl:1047262138202325042>", |
| 54 | "<:pr:1047262146926489691>", | 57 | "<:pr:1047262146926489691>", |
| 55 | "<:bn:1047262139187998790>", | 58 | "<:bn:1047262139187998790>", |
| 56 | "<:yw:1047262152781737986>" | 59 | "<:yw:1047262152781737986>", |
| 60 | "<:gn:1047262141914304633>" | ||
| 57 | }; | 61 | }; |
| 58 | 62 | ||
| 59 | enum FilterDirection { | 63 | enum FilterDirection { |
| @@ -195,6 +199,50 @@ verbly::filter makeHintFilter(verbly::filter subfilter, Height height, Colour co | |||
| 195 | } | 199 | } |
| 196 | break; | 200 | break; |
| 197 | } | 201 | } |
| 202 | case kGreen: { | ||
| 203 | if (filter_direction == kTowardSolution) | ||
| 204 | { | ||
| 205 | verbly::filter whitelist = | ||
| 206 | (verbly::notion::wnid == 109287968) // Geological formations | ||
| 207 | || (verbly::notion::wnid == 109208496) // Asterisms (collections of stars) | ||
| 208 | || (verbly::notion::wnid == 109239740) // Celestial bodies | ||
| 209 | || (verbly::notion::wnid == 109277686) // Exterrestrial objects (comets and meteroids) | ||
| 210 | || (verbly::notion::wnid == 109403211) // Radiators (supposedly natural radiators but actually these are just pictures of radiators) | ||
| 211 | || (verbly::notion::wnid == 109416076) // Rocks | ||
| 212 | || (verbly::notion::wnid == 105442131) // Chromosomes | ||
| 213 | || (verbly::notion::wnid == 100324978) // Tightrope walking | ||
| 214 | || (verbly::notion::wnid == 100326094) // Rock climbing | ||
| 215 | || (verbly::notion::wnid == 100433458) // Contact sports | ||
| 216 | || (verbly::notion::wnid == 100433802) // Gymnastics | ||
| 217 | || (verbly::notion::wnid == 100439826) // Track and field | ||
| 218 | || (verbly::notion::wnid == 100440747) // Skiing | ||
| 219 | || (verbly::notion::wnid == 100441824) // Water sport | ||
| 220 | || (verbly::notion::wnid == 100445351) // Rowing | ||
| 221 | || (verbly::notion::wnid == 100446980) // Archery | ||
| 222 | // TODO: add more sports | ||
| 223 | || (verbly::notion::wnid == 100021939) // Artifacts | ||
| 224 | || (verbly::notion::wnid == 101471682) // Vertebrates | ||
| 225 | ; | ||
| 226 | |||
| 227 | verbly::filter blacklist = | ||
| 228 | (verbly::notion::wnid == 106883725) // swastika | ||
| 229 | || (verbly::notion::wnid == 104416901) // tetraskele | ||
| 230 | || (verbly::notion::wnid == 102512053) // fish | ||
| 231 | || (verbly::notion::wnid == 103575691) // instrument of execution | ||
| 232 | || (verbly::notion::wnid == 103829563) // noose | ||
| 233 | || (verbly::notion::wnid == 103663910) // life support | ||
| 234 | ; | ||
| 235 | |||
| 236 | return subfilter | ||
| 237 | && (verbly::notion::fullHypernyms %= whitelist) | ||
| 238 | && !(verbly::notion::fullHypernyms %= blacklist) | ||
| 239 | && (verbly::notion::partOfSpeech == verbly::part_of_speech::noun) | ||
| 240 | && (verbly::notion::numOfImages >= 1); | ||
| 241 | } else { | ||
| 242 | return (verbly::form::text == "picture"); | ||
| 243 | } | ||
| 244 | break; | ||
| 245 | } | ||
| 198 | default: break; // Not supported yet. | 246 | default: break; // Not supported yet. |
| 199 | } | 247 | } |
| 200 | return {}; | 248 | return {}; |
| @@ -255,6 +303,7 @@ public: | |||
| 255 | dpp::snowflake channel(config["discord_channel"].as<uint64_t>()); | 303 | dpp::snowflake channel(config["discord_channel"].as<uint64_t>()); |
| 256 | 304 | ||
| 257 | database_ = std::make_unique<verbly::database>(config["verbly_datafile"].as<std::string>()); | 305 | database_ = std::make_unique<verbly::database>(config["verbly_datafile"].as<std::string>()); |
| 306 | imagenet_ = std::make_unique<imagenet>(config["imagenet"].as<std::string>()); | ||
| 258 | 307 | ||
| 259 | for (;;) | 308 | for (;;) |
| 260 | { | 309 | { |
| @@ -278,10 +327,12 @@ private: | |||
| 278 | {kMiddle, kRed}, | 327 | {kMiddle, kRed}, |
| 279 | {kMiddle, kBlue}, | 328 | {kMiddle, kBlue}, |
| 280 | {kMiddle, kPurple}, | 329 | {kMiddle, kPurple}, |
| 330 | {kMiddle, kGreen}, | ||
| 281 | {kBottom, kWhite}, | 331 | {kBottom, kWhite}, |
| 282 | {kBottom, kBlack}, | 332 | {kBottom, kBlack}, |
| 283 | {kBottom, kRed}, | 333 | {kBottom, kRed}, |
| 284 | {kBottom, kBlue}, | 334 | {kBottom, kBlue}, |
| 335 | {kBottom, kGreen}, | ||
| 285 | }; | 336 | }; |
| 286 | 337 | ||
| 287 | std::set<std::tuple<Height, Colour>> expensive_hints = { | 338 | std::set<std::tuple<Height, Colour>> expensive_hints = { |
| @@ -313,6 +364,7 @@ private: | |||
| 313 | int non_purple_uses = 0; | 364 | int non_purple_uses = 0; |
| 314 | int expensive_uses = 0; | 365 | int expensive_uses = 0; |
| 315 | int moderate_uses = 0; | 366 | int moderate_uses = 0; |
| 367 | int green_uses = 0; | ||
| 316 | std::array<std::optional<Colour>, kHeightCount> parts; | 368 | std::array<std::optional<Colour>, kHeightCount> parts; |
| 317 | for (int height = 0; height < static_cast<int>(kHeightCount); height++) { | 369 | for (int height = 0; height < static_cast<int>(kHeightCount); height++) { |
| 318 | if (std::bernoulli_distribution(0.5)(rng_)) { | 370 | if (std::bernoulli_distribution(0.5)(rng_)) { |
| @@ -334,6 +386,10 @@ private: | |||
| 334 | { | 386 | { |
| 335 | moderate_uses++; | 387 | moderate_uses++; |
| 336 | } | 388 | } |
| 389 | if (colour == kGreen) | ||
| 390 | { | ||
| 391 | green_uses++; | ||
| 392 | } | ||
| 337 | 393 | ||
| 338 | std::cout << COLOUR_EMOJIS[colour]; | 394 | std::cout << COLOUR_EMOJIS[colour]; |
| 339 | } else { | 395 | } else { |
| @@ -359,6 +415,11 @@ private: | |||
| 359 | std::cout << "Moderate hints can't be combined with an expensive hint." << std::endl; | 415 | std::cout << "Moderate hints can't be combined with an expensive hint." << std::endl; |
| 360 | continue; | 416 | continue; |
| 361 | } | 417 | } |
| 418 | if (green_uses != 1) | ||
| 419 | { | ||
| 420 | std::cout << "Too many green hints." << std::endl; | ||
| 421 | continue; | ||
| 422 | } | ||
| 362 | 423 | ||
| 363 | verbly::filter forwardFilter = cleanFilter && (verbly::form::proper == false); | 424 | verbly::filter forwardFilter = cleanFilter && (verbly::form::proper == false); |
| 364 | for (int i=0; i<static_cast<int>(kHeightCount); i++) { | 425 | for (int i=0; i<static_cast<int>(kHeightCount); i++) { |
| @@ -401,10 +462,24 @@ private: | |||
| 401 | std::cout << message_text << std::endl << std::endl << solution.getText() << std::endl; | 462 | std::cout << message_text << std::endl << std::endl << solution.getText() << std::endl; |
| 402 | 463 | ||
| 403 | std::vector<verbly::form> admissibleResults = database_->forms(admissible, {}, 10).all(); | 464 | std::vector<verbly::form> admissibleResults = database_->forms(admissible, {}, 10).all(); |
| 404 | if (admissibleResults.size() <= (hints == 1 ? 2 : 5)) | 465 | if (green_uses > 0 || (admissibleResults.size() <= (hints == 1 ? 2 : 5))) |
| 405 | { | 466 | { |
| 406 | #ifdef ENABLE_BOT | 467 | #ifdef ENABLE_BOT |
| 407 | dpp::message message(channel, message_text); | 468 | dpp::message message(channel, message_text); |
| 469 | |||
| 470 | if (green_uses > 0) | ||
| 471 | { | ||
| 472 | verbly::notion notion = database_->notions( | ||
| 473 | (verbly::notion::numOfImages > 1) && solution).first(); | ||
| 474 | auto [image, extension] = imagenet_->getImageForNotion(notion.getId(), rng_); | ||
| 475 | if (image.empty()) | ||
| 476 | { | ||
| 477 | throw std::runtime_error("Could not find image for green hint."); | ||
| 478 | } | ||
| 479 | |||
| 480 | message.add_file(std::string("SPOILER_image.") + extension, image); | ||
| 481 | } | ||
| 482 | |||
| 408 | bot_->message_create(message, [this, &solution](const dpp::confirmation_callback_t& userdata) { | 483 | bot_->message_create(message, [this, &solution](const dpp::confirmation_callback_t& userdata) { |
| 409 | const auto& posted_msg = std::get<dpp::message>(userdata.value); | 484 | const auto& posted_msg = std::get<dpp::message>(userdata.value); |
| 410 | std::lock_guard answer_lock(answers_mutex_); | 485 | std::lock_guard answer_lock(answers_mutex_); |
| @@ -432,6 +507,7 @@ private: | |||
| 432 | std::mt19937& rng_; | 507 | std::mt19937& rng_; |
| 433 | std::unique_ptr<dpp::cluster> bot_; | 508 | std::unique_ptr<dpp::cluster> bot_; |
| 434 | std::unique_ptr<verbly::database> database_; | 509 | std::unique_ptr<verbly::database> database_; |
| 510 | std::unique_ptr<imagenet> imagenet_; | ||
| 435 | std::map<uint64_t, std::string> answer_by_message_; | 511 | std::map<uint64_t, std::string> answer_by_message_; |
| 436 | std::mutex answers_mutex_; | 512 | std::mutex answers_mutex_; |
| 437 | }; | 513 | }; |
| diff --git a/vendor/curlcpp b/vendor/curlcpp new file mode 160000 | |||
| Subproject f4b87d02725065536bf6620c29aa8c13e15a199 | |||
| diff --git a/vendor/verbly b/vendor/verbly | |||
| Subproject f2731325f551c4cfea861e2e31d214936b9bd61 | Subproject 06e4672540094a851542b47abaf022f934b63b0 | ||
