Jeff Ted
Jeff Ted

Reputation: 61

Timed input in python 3 for Mac and Windows

I need a timed input for python 3 on both Mac and Windows which, if the user runs out of time, automatically chooses one of the options. Every other answer for a question I've seen only terminates after user presses 'Enter'. I need it to terminate regardless of any 'Enter' pressed or not.

For example, in my chess program, if no input is given after 5 seconds, I need it to automatically choose "Q". Also if the input is not in ["Q", "R", "B", "N"] then I also need it to choose "Q".

The following code doesn't work. It'll ask for the input then wait forever; the timer doesn't work at all.

def countdown():
    global timer
    timer = 5
    for x in range(timer):
        timer -= 1
        time.sleep(1)

countdownThread = threading.Thread(target=countdown)
countdownThread.start()

while timer > 0:
    promotedPiece = input("Promote to Q, R, B or N? >  ").upper()
    if timer == 0:
        break

if timer == 0:
    if promotedPiece not in ["Q", "R", "B", "N"]:  # If not valid input, choose Queen
        promotedPiece = "Q"
        print("Queen auto selected")

Upvotes: 6

Views: 685

Answers (4)

Kovy Jacob
Kovy Jacob

Reputation: 1117

I've spent a few hours, and this is as far as I got:

import time
from multiprocessing import Process

global timer
timer = 5

def get_answer():
    answer = input ("ANSWER THE QUESTION!!!")

def runtime():
    global timer
    while True:
        time.sleep(1)
        timer = timer-1
        print (timer)
        if timer == 0:
            thread1.terminate()
            thread1.join()
            print ("hi")
            break

# create two new threads
thread1 = Process(target=get_answer, args=())
thread2 = Process(target=runtime, args=())

# start the threads
thread1.start()
thread2.start()

# wait for the threads to complete
thread1.join()
thread2.join()

All you need to do is work out the error I'm getting with the Process module, and you'll be good.

Upvotes: 0

vgel
vgel

Reputation: 3345

If you're comfortable with async, it's a very simple solution to this problem, with the aioconsole (pip install aioconsole) library:

import asyncio

from aioconsole import ainput # async version of input()

async def get_choice() -> str:
    try:
        coro = ainput("Promote to Q, R, B or N? >  ")
        choice = await asyncio.wait_for(coro, timeout=5.0)
    except asyncio.TimeoutError:
        print()  # go to next line past ">" of prompt
        choice = "Q"
    return choice.upper()

async def main():
    print("You chose", await get_choice())

asyncio.run(main())

Upvotes: 2

azelcer
azelcer

Reputation: 1533

As managing input in different environments is quite different, it is a good idea to use a module where someone else has taken care of it. pynput does what you want. Be careful as it has some caveats.

Here is the implementation of timeout_input and timeout_getchar functions that are good enough for general use. In your case, timeout_getchar should be a perfect match:

from pynput import keyboard
from time import monotonic


def timeout_input(timeout:float, prompt:str='', echo=True, None_on_timeout=False):
    """Read string with timeout.
    
    Reads keypresses until enter is pressed or timeout occurs. On enter,
    returns the characters read. On timeout the return value depends on the
    None_on_timeout parameter.

    Parameters
    ----------
    timeout : float
        Timeout in seconds
    prompt : str
        Prompt to print
    echo : bool
        If True, echo keys to screen as they are pressed
    None_on_timeout : bool
        If this parameter is True and timeout is reached, None is returned.
        Otherwise a (possibly empty) str of characters read so far is returned

    Returns
    -------
    str or None
        Characters read

    Observations
    ------------
    Requires pynput. All caveats of pynput apply to this function. As it
    records keypresses some differences with plain input behavior are expected.
    """
    rv = []
    print(prompt, end='')
    with keyboard.Events() as events:
        while timeout > 0:
            start_time = monotonic()
            event = events.get(timeout)
            if event is not None:
                if isinstance(event, keyboard.Events.Press):
                    try:
                        if isinstance(event.key.char, str):
                            if echo:
                                print(event.key.char, end='')
                            rv.append(event.key.char)
                    except AttributeError:
                        if event.key == keyboard.Key.enter:
                            return ''.join(rv)
            timeout -= monotonic() - start_time
    return None if None_on_timeout else ''.join(rv)


def timeout_getchar(timeout:float, prompt:str=''):
    """Read a single char with timeout.
    
    On timeout the return value is None

    Parameters
    ----------
    timeout : float
        Timeout in seconds
    prompt : str
        Prompt to print

    Returns
    -------
    str or None
        Character read

    Observations
    ------------
    Requires pynput. All caveats of pynput apply to this function. As it
    records keypresses some differences with plain input behavior are expected
    """
    print(prompt, end='')
    with keyboard.Events() as events:
        while timeout > 0:
            start_time = monotonic()
            event = events.get(timeout)
            if event is not None:
                if isinstance(event, keyboard.Events.Press):
                    try:
                        if isinstance(event.key.char, str):
                            return event.key.char
                    except AttributeError:
                        pass
            timeout -= monotonic() - start_time
    return None

promotedPiece = timeout_getchar(5, "Promote to Q, R, B or N? >  ")
print()
if promotedPiece is None:
    print("You took too long. I choose Q for you")
    promotedPiece = 'Q'
promotedPiece = promotedPiece.upper()
if promotedPiece not in ["Q", "R", "B", "N"]:  # If not valid input, choose Queen
    print(f"I said Q, R, B or N. You can not choose {promotedPiece}. I choose Q for you")
    promotedPiece = 'Q'
print("selection = ", promotedPiece)

Upvotes: 0

Thymen
Thymen

Reputation: 2179

This is a very interesting question, and many people have been looking at timed inputs:

About the retrieving of a single character very good answers are provided for all platforms:

Solutions

Linux

The general consensus is that when you are using Linux the following works good (taken from jer's answer):

import signal

class AlarmException(Exception):
    pass

def alarmHandler(signum, frame):
    raise AlarmException

def nonBlockingRawInput(prompt='', timeout=20):
    signal.signal(signal.SIGALRM, alarmHandler)
    signal.alarm(timeout)
    try:
        text = raw_input(prompt)
        signal.alarm(0)
        return text
    except AlarmException:
        print '\nPrompt timeout. Continuing...'
    signal.signal(signal.SIGALRM, signal.SIG_IGN)
    return ''

Windows

But when you are on Windows, there is no real good solution. People have been trying to use threads and interrupt signals, but when I tried them out I couldn't get them working properly. Most direct solutions are summarized in this gist from atupal. But there are many more attempts:

The last thing I could come up with for your specific situation is to simulate a key press after the timeout. This key press will then be the user input (even though the user didn't input one, for the program it seems like he did).

For this solution I created the following files:


  • listener.py: with the code from https://stackoverflow.com/a/510364/10961342

      class _Getch:
          """Gets a single character from standard input.  Does not echo to the
      screen."""
    
          def __init__(self):
              try:
                  self.impl = _GetchWindows()
              except ImportError:
                  self.impl = _GetchUnix()
    
          def __call__(self):
              return self.impl()
    
    
      class _GetchUnix:
          def __init__(self):
              import tty, sys
    
          def __call__(self):
              import sys, tty, termios
              fd = sys.stdin.fileno()
              old_settings = termios.tcgetattr(fd)
              try:
                  tty.setraw(sys.stdin.fileno())
                  ch = sys.stdin.read(1)
              finally:
                  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
              return ch
    
    
      class _GetchWindows:
          def __init__(self):
              import msvcrt
    
          def __call__(self):
              import msvcrt
              return msvcrt.getch()
    
    
      getch = _Getch()
    

  • keys.py: with (unicode) from https://stackoverflow.com/a/13290031/10961342

    (from the question: How to generate keyboard events?)

      import ctypes
      import time
    
      SendInput = ctypes.windll.user32.SendInput
    
      PUL = ctypes.POINTER(ctypes.c_ulong)
    
      KEYEVENTF_UNICODE = 0x0004
      KEYEVENTF_KEYUP = 0x0002
    
    
      class KeyBdInput(ctypes.Structure):
          _fields_ = [("wVk", ctypes.c_ushort),
                      ("wScan", ctypes.c_ushort),
                      ("dwFlags", ctypes.c_ulong),
                      ("time", ctypes.c_ulong),
                      ("dwExtraInfo", PUL)]
    
    
      class HardwareInput(ctypes.Structure):
          _fields_ = [("uMsg", ctypes.c_ulong),
                      ("wParamL", ctypes.c_short),
                      ("wParamH", ctypes.c_ushort)]
    
    
      class MouseInput(ctypes.Structure):
          _fields_ = [("dx", ctypes.c_long),
                      ("dy", ctypes.c_long),
                      ("mouseData", ctypes.c_ulong),
                      ("dwFlags", ctypes.c_ulong),
                      ("time", ctypes.c_ulong),
                      ("dwExtraInfo", PUL)]
    
    
      class Input_I(ctypes.Union):
          _fields_ = [("ki", KeyBdInput),
                      ("mi", MouseInput),
                      ("hi", HardwareInput)]
    
    
      class Input(ctypes.Structure):
          _fields_ = [("type", ctypes.c_ulong),
                      ("ii", Input_I)]
    
    
      def PressKey(KeyUnicode):
          extra = ctypes.c_ulong(0)
          ii_ = Input_I()
          ii_.ki = KeyBdInput(0, KeyUnicode, KEYEVENTF_UNICODE, 0, ctypes.pointer(extra))
          x = Input(ctypes.c_ulong(1), ii_)
          ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
    
    
      def ReleaseKey(KeyUnicode):
          extra = ctypes.c_ulong(0)
          ii_ = Input_I()
          ii_.ki = KeyBdInput(0, KeyUnicode, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, ctypes.pointer(extra))
          x = Input(ctypes.c_ulong(1), ii_)
          ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))
    
    
      def PressAltTab():
          PressKey(0x012)  # Alt
          PressKey(0x09)  # Tab
    
          time.sleep(2)  # optional : if you want to see the atl-tab overlay
    
          ReleaseKey(0x09)  # ~Tab
          ReleaseKey(0x012)  # ~Alt
    
    
      if __name__ == "__main__":
          PressAltTab()
    

  • main.py: with the following

    import time
    import multiprocessing as mp
    import listener
    import keys
    
    
    def press_key(delay=2, key='Q'):
        time.sleep(delay)
    
        # All keys https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes?redirectedfrom=MSDN
        keys.PressKey(int(hex(ord(key)), base=16))  # 0x51
        keys.ReleaseKey(int(hex(ord(key)), base=16))  # 0x51
    
    
    if __name__ == '__main__':
        process = mp.Process(target=press_key)
        process.start()
    
        question = "Promote to?"
        choice = ('Q', 'B', 'R', 'N')
    
        print(f"Promote to? {choice}\n>>> ", end='')
        reply = (listener.getch()).upper().decode('utf-8')
        process.terminate()
    
        if reply not in choice:
            reply = 'Q'
    
        print(f"Promoted piece to {reply}")
    

Explanation

listener makes sure to only listen to a single key stroke, while keys gives us the option to simulate a user key. The main calls a Process that will automatically send a Q to the stdin after a certain delay in seconds. A process has been made so we can call process.terminate() before the Q is send, if we have a user reply (much more complicated with a thread).

Notes

  • This only works if you directly read from the cmd console. If you use an IDE, you have to emulate the terminal. For example in Pycharm, you have to select Emulate terminal in output console in Run/Debug Configurations.

  • The terminal window has to be selected, otherwise the Q will be send to whatever window is currently active.

Promote to? ('Q', 'B', 'R', 'N')
>>> Promoted piece to Q

Process finished with exit code 0
Promote to? ('Q', 'B', 'R', 'N')
>>> Promoted piece to N

Process finished with exit code 0

Gui alternatives

Even though it is hard to make it work directly in python, there are alternative solutions when you take the whole program into account. The question seems to refer to chess, and when you build a full game (with gui) you can often use their build in keyboard capture. For example:

Then you can set a variable promoting and if that is true, return the pressed key or an alternative key after the wait period.


Edit

A minimal working example in pygame would be:

import pygame


def set_timer(timeout=5):
    print(f"To which piece do you want to promote?")
    pygame.time.set_timer(pygame.USEREVENT, timeout * 1_000)
    return True


def check_timer_keys(event):
    if event.type == pygame.KEYDOWN:
        if event.unicode.upper() in ['Q', 'R', 'N', 'B']:
            print(f'Key press {event}')
            return False
        else:
            print(f"Invalid key! {event}")
            return True

    if event.type == pygame.USEREVENT:
        print("Exceeded time, user event")
        return False

    return True


if __name__ == '__main__':
    pygame.init()
    screen = pygame.display.set_mode((500, 500))
    running = True
    timer = False

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

            # Should be before the T call, other wise it also handles the `t` call.
            if timer:
                timer = check_timer_keys(event)

            # When pressing T, start the promotion question.
            if event.type == pygame.KEYDOWN:
                if event.unicode.upper() == 'T':
                    timer = set_timer(timeout=2)

        screen.fill((255, 255, 255))
        pygame.display.flip()

    pygame.quit()

Whenever you press the t key a timer start to run with a 2 second timeout. This means that after 2 seconds you will get a custom pygame.USEREVENT, which means that the user failed to answer within the time. In the check_timer_keys I filter for the Q, R, N and B key, hence other keys are not processed for this specific question.

A current limitation is that you can mix the USEREVENT call if you promote multiple pieces within the timeout period. If you want to prevent this, you have to add a counter that indicates how many USEREVENT's call are estill being processed (if number is larger than 1, wait until it is 1).

Upvotes: 7

Related Questions