3

I am trying to create a timed quiz game. If the timer runs out the game should immediatly stop the quiz even if the user was typing something. Right now the user can still input a last awnser before the game ends.

How can I make the cin non blocking? Is there a simple way of doing this?

My code:

#include <iostream>
#include <thread>
#include <chrono>

using namespace std::chrono_literals;

bool timeOver = false;

void countDown()
{
    const auto timer_duration = 5s;
    const auto start = std::chrono::steady_clock::now();

    std::chrono::duration<double> time_left = timer_duration - (std::chrono::steady_clock::now() - start);
    while (time_left > 0s)
    {
        std::this_thread::sleep_for(1s);
        time_left = timer_duration - (std::chrono::steady_clock::now() - start);
    }
    timeOver = true;
}

void quiz()
{
    std::string anwser;
    while(!timeOver)
    {
        std::cout << "some question?\n";
        std::cin >> anwser;
    }
    std::cout << "time's up\n";
}

int main()
{
    std::thread th1(countDown);
    std::thread th2(quiz);
    std::cout << "both threads have started\n";
    th1.join();
    th2.join();
    std::cout << "both threads have ended\n";
}

somedude
  • 53
  • 3
  • Does this answer your question? [Is it possible to set timeout for std::cin?](https://stackoverflow.com/questions/9053175/is-it-possible-to-set-timeout-for-stdcin) – Marco F. Jul 12 '23 at 10:15
  • more difficult than you might expect. For a simple alternative consider to measure the time the user needs to answer the question, and then the answer is either in time or too late, but they need to type the answer in any case – 463035818_is_not_an_ai Jul 12 '23 at 10:22

2 Answers2

0

I unfortunately cannot think of any portable possibility. The problem is the fact that you try to read from the stream and that reading cannot be timed out or wakened - in standard c++ (or standard c).

What is possible though is to use the non portable select. With select you can listen for more than just one file descriptor. You can then create a pipe (also non portable) and add the read end of the pipe to the file descriptors you're listening on. That way you can wake up the thread that listens on stdin.

Maybe it's a better solution to go for the alternative mentioned in the comments, that way you just have to wait for the complete input but that might not be an issue, since you can evaluate whether the answer was provided in the given time slot or not. That way you could use portable c++.

The non portable solution could look like this:

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

#ifdef WIN32
#    include <io.h>
#    include <winsock2.h>
#else
#    include <sys/select.h>
#    include <unistd.h>
#endif

using namespace std::chrono_literals;

struct Quiz
{
    int pipe[2];
};

void countDown (const Quiz& quiz)
{
    std::this_thread::sleep_for (5s);
    std::cout << "Notify quiz...\n";
    char trigger {'s'};
#ifdef WIN32
    _write (quiz.pipe[1], &trigger, sizeof trigger);
#else
    write (quiz.pipe[1], &trigger, sizeof trigger);
#endif
}

void quiz (const Quiz& quiz)
{
    while (true) {
        fd_set fds;
        FD_ZERO (&fds);
        FD_SET (0, &fds);
        FD_SET (quiz.pipe[0], &fds);

        std::cout << "some question?\n";

        int result = select (quiz.pipe[0] + 1, &fds, nullptr, nullptr, nullptr);
        if (result < 0) {
            std::cerr << "Uh oh, problem in select\n";
            return;
        }

        if (FD_ISSET (quiz.pipe[0], &fds)) {
            std::cout << "time's up\n";
            return;
        }
        else {
            std::string answer;
            std::cin >> answer;
            std::cout << "your answer: " << answer << '\n';
        }
    }
    std::cout << "time's up\n";
}

int main ()
{
    Quiz bucket;

#ifdef WIN32
    _pipe (quiz.pipe, 256, O_BINARY);
#else
    pipe (bucket.pipe);
#endif

    std::thread th1 {&countDown, std::cref (bucket)};
    std::thread th2 {&quiz, std::cref (bucket)};
    std::cout << "both threads have started\n";
    th1.join ();
    th2.join ();
    std::cout << "both threads have ended\n";

#ifdef WIN32
    _close (quiz.pipe[1]);
    _close (quiz.pipe[0]);
#else
    close (bucket.pipe[1]);
    close (bucket.pipe[0]);
#endif
}

I want to mention that I changed the plain bool to an atomic boolean in order to eliminate possible race conditions.

David
  • 93
  • 2
  • 7
-1

Well... Here's a CPU draining implementation that works. Just to be clear, I would advise against such approach but it gets the job done for this example.

The main problem is that std::cin is going to block because it will wait for all the characters to be inserted by the user before buffering them, otherwise it would have been very inefficient. What you can do is spawn a thread (cpu draining part) every time you ask a question, but you must detach the thread so that the main thread doesn't wait for it to be joined. The detached thread will die as soon as an input is entered.

#include <iostream>
#include <thread>
#include <chrono>

bool timeOver = false;

void countDown()
{
    auto timer_duration = std::chrono::seconds(5);
    auto start = std::chrono::steady_clock::now();

    std::chrono::duration<double> time_left = timer_duration - (std::chrono::steady_clock::now() - start);
    while (time_left > std::chrono::seconds(0))
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        time_left = timer_duration - (std::chrono::steady_clock::now() - start);
    }
    timeOver = true;
}

void quiz()
{
    std::string answer;
    bool asked = false;
    while(!timeOver)
    {
        if (!asked)
        {
            std::thread([&answer]()
                        {
                            std::string asnw;
                            std::cout << "some question?\n";
                            std::cin >> asnw;
                            answer = asnw;
                        }).detach();
            asked = true;
        }

        if (!answer.empty())
        {
            std::cout<< "responded with:" << answer << std::flush << std::endl;
            answer.clear();
            asked = false;
        }
    }
    std::cout << "time's up!\n"; 
}

int main() 
{
    std::thread t(countDown);
    quiz();
    t.join();
}

The results are:

$ ./a.out
some question?
foo
responded with:foo
some question?
bar
responded with:bar
some question?
tteeeeeeeeeeetime's up! //<--- killed before I could finish the answer
Constantinos Glynos
  • 2,952
  • 2
  • 14
  • 32