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 | ||