diff options
-rw-r--r-- | lingo.cpp | 153 |
1 files changed, 122 insertions, 31 deletions
diff --git a/lingo.cpp b/lingo.cpp index 1cb13ce..04c2921 100644 --- a/lingo.cpp +++ b/lingo.cpp | |||
@@ -302,6 +302,13 @@ private: | |||
302 | std::vector<std::tuple<std::string, std::string>> puzzles_; | 302 | std::vector<std::tuple<std::string, std::string>> puzzles_; |
303 | }; | 303 | }; |
304 | 304 | ||
305 | struct puzzle { | ||
306 | std::string message; // exact text to be posted to discord | ||
307 | std::string solution; // pre-canonicalized for answer checking | ||
308 | std::string attachment_name; // only populated for green clues | ||
309 | std::string attachment_content; // ^ | ||
310 | }; | ||
311 | |||
305 | class lingo { | 312 | class lingo { |
306 | public: | 313 | public: |
307 | lingo(std::mt19937& rng) : rng_(rng) {} | 314 | lingo(std::mt19937& rng) : rng_(rng) {} |
@@ -325,8 +332,7 @@ public: | |||
325 | if (event.command.get_command_name() == "newpuzzle") { | 332 | if (event.command.get_command_name() == "newpuzzle") { |
326 | event.thinking(true); | 333 | event.thinking(true); |
327 | 334 | ||
328 | std::thread genpuzz(&lingo::generatePuzzle, this, event.command.get_channel().id); | 335 | sendPuzzle(event.command.get_channel().id); |
329 | genpuzz.detach(); | ||
330 | } | 336 | } |
331 | }); | 337 | }); |
332 | 338 | ||
@@ -335,7 +341,7 @@ public: | |||
335 | uint64_t puzzle_id = static_cast<uint64_t>(event.msg.message_reference.message_id); | 341 | uint64_t puzzle_id = static_cast<uint64_t>(event.msg.message_reference.message_id); |
336 | if (answer_by_message_.count(puzzle_id)) | 342 | if (answer_by_message_.count(puzzle_id)) |
337 | { | 343 | { |
338 | std::string canonical_answer = hatkirby::lowercase(answer_by_message_[puzzle_id]); | 344 | std::string canonical_answer = answer_by_message_[puzzle_id]; |
339 | std::string canonical_attempt = hatkirby::lowercase(event.msg.content); | 345 | std::string canonical_attempt = hatkirby::lowercase(event.msg.content); |
340 | while (canonical_attempt.find("||") != std::string::npos) | 346 | while (canonical_attempt.find("||") != std::string::npos) |
341 | { | 347 | { |
@@ -345,10 +351,6 @@ public: | |||
345 | { | 351 | { |
346 | canonical_attempt.erase(canonical_attempt.find(" "), 1); | 352 | canonical_attempt.erase(canonical_attempt.find(" "), 1); |
347 | } | 353 | } |
348 | while (canonical_answer.find(" ") != std::string::npos) | ||
349 | { | ||
350 | canonical_answer.erase(canonical_answer.find(" "), 1); | ||
351 | } | ||
352 | 354 | ||
353 | std::cout << "\"" << canonical_attempt << "\"" << std::endl; | 355 | std::cout << "\"" << canonical_attempt << "\"" << std::endl; |
354 | if (canonical_attempt == canonical_answer) | 356 | if (canonical_attempt == canonical_answer) |
@@ -418,17 +420,18 @@ public: | |||
418 | 420 | ||
419 | for (;;) | 421 | for (;;) |
420 | { | 422 | { |
421 | std::thread genpuzz(&lingo::generatePuzzle, this, channel); | 423 | sendPuzzle(channel); |
422 | genpuzz.detach(); | ||
423 | 424 | ||
424 | std::this_thread::sleep_for(std::chrono::hours(24)); | 425 | std::this_thread::sleep_for(std::chrono::hours(4)); |
425 | } | 426 | } |
426 | } | 427 | } |
427 | 428 | ||
428 | private: | 429 | private: |
429 | 430 | ||
430 | void generatePuzzle(dpp::snowflake channel) | 431 | void generatePuzzle() |
431 | { | 432 | { |
433 | std::cout << "Generating puzzle..." << std::endl; | ||
434 | |||
432 | std::set<std::tuple<Height, Colour>> filters = { | 435 | std::set<std::tuple<Height, Colour>> filters = { |
433 | {kTop, kPurple}, | 436 | {kTop, kPurple}, |
434 | {kTop, kWhite}, | 437 | {kTop, kWhite}, |
@@ -469,8 +472,8 @@ private: | |||
469 | !(verbly::word::usageDomains %= (verbly::notion::wnid == 106718862)) // ethnic slurs | 472 | !(verbly::word::usageDomains %= (verbly::notion::wnid == 106718862)) // ethnic slurs |
470 | && !(verbly::notion::wnid == 110630093); // "spastic" | 473 | && !(verbly::notion::wnid == 110630093); // "spastic" |
471 | 474 | ||
472 | bool generated = false; | 475 | std::unique_ptr<puzzle> genpuzzle; |
473 | while (!generated) | 476 | for (;;) |
474 | { | 477 | { |
475 | std::cout << "Generating... " << std::endl; | 478 | std::cout << "Generating... " << std::endl; |
476 | try | 479 | try |
@@ -603,8 +606,13 @@ private: | |||
603 | std::vector<verbly::form> admissibleResults = database_->forms(admissible, {}, 10).all(); | 606 | std::vector<verbly::form> admissibleResults = database_->forms(admissible, {}, 10).all(); |
604 | if (green_uses > 0 || (admissibleResults.size() <= (hints == 1 ? 2 : 5))) | 607 | if (green_uses > 0 || (admissibleResults.size() <= (hints == 1 ? 2 : 5))) |
605 | { | 608 | { |
606 | #ifdef ENABLE_BOT | 609 | genpuzzle = std::make_unique<puzzle>(); |
607 | dpp::message message(channel, message_text); | 610 | genpuzzle->message = message_text; |
611 | genpuzzle->solution = hatkirby::lowercase(solution.getText()); | ||
612 | while (genpuzzle->solution.find(" ") != std::string::npos) | ||
613 | { | ||
614 | genpuzzle->solution.erase(genpuzzle->solution.find(" "), 1); | ||
615 | } | ||
608 | 616 | ||
609 | if (green_uses > 0) | 617 | if (green_uses > 0) |
610 | { | 618 | { |
@@ -616,21 +624,11 @@ private: | |||
616 | throw std::runtime_error("Could not find image for green hint."); | 624 | throw std::runtime_error("Could not find image for green hint."); |
617 | } | 625 | } |
618 | 626 | ||
619 | message.add_file(std::string("SPOILER_image.") + extension, image); | 627 | genpuzzle->attachment_name = std::string("SPOILER_image.") + extension; |
628 | genpuzzle->attachment_content = std::move(image); | ||
620 | } | 629 | } |
621 | 630 | ||
622 | bot_->message_create(message, [this, &solution](const dpp::confirmation_callback_t& userdata) { | 631 | break; |
623 | const auto& posted_msg = std::get<dpp::message>(userdata.value); | ||
624 | std::lock_guard answer_lock(answers_mutex_); | ||
625 | if (answer_by_message_.size() > 3000) | ||
626 | { | ||
627 | answer_by_message_.clear(); | ||
628 | } | ||
629 | answer_by_message_[posted_msg.id] = solution.getText(); | ||
630 | }); | ||
631 | #endif | ||
632 | |||
633 | generated = true; | ||
634 | } else { | 632 | } else { |
635 | std::cout << "Too many (" << admissibleResults.size() << ") results." << std::endl; | 633 | std::cout << "Too many (" << admissibleResults.size() << ") results." << std::endl; |
636 | } | 634 | } |
@@ -641,18 +639,111 @@ private: | |||
641 | std::cout << "Waiting five seconds then trying again..." << std::endl; | 639 | std::cout << "Waiting five seconds then trying again..." << std::endl; |
642 | std::this_thread::sleep_for(std::chrono::seconds(5)); | 640 | std::this_thread::sleep_for(std::chrono::seconds(5)); |
643 | } | 641 | } |
642 | |||
643 | // generatePuzzle is only called when there is no cached puzzle and there | ||
644 | // is no other puzzle being generated. therefore, we do not need to worry | ||
645 | // about a race condition with another generatePuzzle. however, we do not | ||
646 | // want to interfere with a sendPuzzle. lock the cached puzzle. if | ||
647 | // sendPuzzle gets it first, it will see that there is no cached puzzle, | ||
648 | // it will queue its channel, and then it will return because a puzzle is | ||
649 | // already being generated. if generatePuzzle gets it first and there are | ||
650 | // no queued channels, it will store the puzzle and return, which means | ||
651 | // that a waiting sendPuzzle can immediately grab it. if there is a | ||
652 | // queued channel, generatePuzzle dequeues it and and calls sendPuzzle on | ||
653 | // it. which sendPuzzle gets the lock is undetermined, but it does not | ||
654 | // matter. | ||
655 | std::optional<dpp::snowflake> send_channel; | ||
656 | { | ||
657 | std::lock_guard cache_lock(cache_mutex_); | ||
658 | cached_puzzle_ = std::move(genpuzzle); | ||
659 | generating_puzzle_ = false; | ||
660 | |||
661 | if (!queued_channels_.empty()) | ||
662 | { | ||
663 | send_channel = queued_channels_.front(); | ||
664 | queued_channels_.pop_front(); | ||
665 | } | ||
666 | } | ||
667 | |||
668 | if (send_channel) | ||
669 | { | ||
670 | sendPuzzle(*send_channel); | ||
671 | } | ||
672 | } | ||
673 | |||
674 | // called when the bot starts (no cached puzzle) | ||
675 | // called when /newpuzzle is run (maybe a cached puzzle) | ||
676 | // called at the end of generatePuzzle if channel queue is non-empty (definitely a cached puzzle) | ||
677 | // | ||
678 | // if there is a cached puzzle, send it immediately (removes cached puzzle). if not, add it to the channels queue. | ||
679 | // at this point there is no cached puzzle. if there is no puzzle being generated right now, spin off a thread to do so | ||
680 | void sendPuzzle(dpp::snowflake channel) | ||
681 | { | ||
682 | // lock the cached puzzle so | ||
683 | // 1) sendPuzzle in another thread doesn't steal our puzzle | ||
684 | // 2) generatePuzzle in another thread doesn't provide a puzzle | ||
685 | // before we can add something to the channel queue | ||
686 | std::lock_guard cache_lock(cache_mutex_); | ||
687 | if (cached_puzzle_ != nullptr) | ||
688 | { | ||
689 | std::cout << "Sending to " << static_cast<uint64_t>(channel) << std::endl; | ||
690 | #ifdef ENABLE_BOT | ||
691 | dpp::message message(channel, cached_puzzle_->message); | ||
692 | if (!cached_puzzle_->attachment_content.empty()) | ||
693 | { | ||
694 | message.add_file(cached_puzzle_->attachment_name, cached_puzzle_->attachment_content); | ||
695 | } | ||
696 | |||
697 | std::string solution = cached_puzzle_->solution; | ||
698 | bot_->message_create(message, [this, solution](const dpp::confirmation_callback_t& userdata) { | ||
699 | const auto& posted_msg = std::get<dpp::message>(userdata.value); | ||
700 | std::lock_guard answer_lock(answers_mutex_); | ||
701 | if (answer_by_message_.size() > 3000) | ||
702 | { | ||
703 | answer_by_message_.clear(); | ||
704 | } | ||
705 | answer_by_message_[posted_msg.id] = solution; | ||
706 | }); | ||
707 | #endif | ||
708 | |||
709 | cached_puzzle_.reset(); | ||
710 | } else { | ||
711 | std::cout << "Queued " << static_cast<uint64_t>(channel) << std::endl; | ||
712 | queued_channels_.push_back(channel); | ||
713 | } | ||
714 | |||
715 | // at this point, we guarantee that there are no puzzles. a generation | ||
716 | // thread may already be running, though, so we will not spin up | ||
717 | // another one if there is. | ||
718 | if (generating_puzzle_) | ||
719 | { | ||
720 | return; | ||
721 | } | ||
722 | |||
723 | generating_puzzle_ = true; | ||
724 | |||
725 | std::thread generation_thread([this](){ | ||
726 | generatePuzzle(); | ||
727 | }); | ||
728 | generation_thread.detach(); | ||
644 | } | 729 | } |
645 | 730 | ||
646 | std::mt19937& rng_; | 731 | std::mt19937& rng_; |
647 | std::unique_ptr<dpp::cluster> bot_; | 732 | std::unique_ptr<dpp::cluster> bot_; |
648 | std::unique_ptr<verbly::database> database_; | 733 | std::unique_ptr<verbly::database> database_; |
649 | std::unique_ptr<imagenet> imagenet_; | 734 | std::unique_ptr<imagenet> imagenet_; |
650 | std::map<uint64_t, std::string> answer_by_message_; | ||
651 | std::set<uint64_t> solved_puzzles_; | ||
652 | std::mutex answers_mutex_; | ||
653 | std::string scoreboard_endpoint_; | 735 | std::string scoreboard_endpoint_; |
654 | std::string scoreboard_secret_code_; | 736 | std::string scoreboard_secret_code_; |
655 | std::unique_ptr<wanderlust> wanderlust_; | 737 | std::unique_ptr<wanderlust> wanderlust_; |
738 | |||
739 | std::map<uint64_t, std::string> answer_by_message_; | ||
740 | std::set<uint64_t> solved_puzzles_; | ||
741 | std::mutex answers_mutex_; | ||
742 | |||
743 | bool generating_puzzle_ = false; | ||
744 | std::unique_ptr<puzzle> cached_puzzle_; | ||
745 | std::deque<dpp::snowflake> queued_channels_; | ||
746 | std::mutex cache_mutex_; | ||
656 | }; | 747 | }; |
657 | 748 | ||
658 | int main(int argc, char** argv) | 749 | int main(int argc, char** argv) |