1

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")
Jeff Ted
  • 11
  • 2
  • You might want to build this off the events in the `keyboard` module rather than the builtin `input` function. – Samwise Feb 14 '21 at 19:12

1 Answers1

1

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://docs.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).

Thymen
  • 1,471
  • 1
  • 5
  • 10