Остановить сканер от ожидания ввода - PullRequest
0 голосов
/ 29 июня 2018

Цель

В настоящее время я создаю (для практики с Java) базовую многопользовательскую пошаговую игру для командной строки. В этой игре у каждого игрока есть 5 секунд, чтобы сделать свой ход. Когда он делает свой ход (или , когда заканчивается таймер ), другой игрок начинает свой ход и т. Д. И т. Д. Сервер отправляет сообщение TimerEnded каждый раз, когда заканчивается таймер. Моя текущая цель - добиться безошибочного чтения ввода, чтобы мог прерваться , когда клиенту придет сообщение TimerEnded.

Дизайн

Чтобы достичь этого, я создал синглтон под названием InputManager. Этот класс обрабатывает все входные данные для чтения. Я создал метод с именем ask, который принимает обратный вызов в качестве параметра. В этом методе я создаю новый поток, и внутри него я жду ввода с Scanner.hasNextInt. Этот класс также имеет метод closeInput, который отправляет сообщение прерывания потоку, описанному выше. Вот текущая реализация класса:

class InputManager{
    private Thread thread;
    private InputManager(){}
    private static InputManager instance;
    private static InputManager getInstance(){
        if(instance == null){
            instance = new InputManager();
        }
        return instance;
    }

    /**
     * Ask user to type a number.
     * @param onSelected When the user has made his choice, this callback will be executed
     */
    public static void ask( Consumer<Integer> onSelected){
        getInstance().thread = new Thread(() -> {
            System.out.println("Type a number:");

            Scanner sc = new Scanner(System.in);
            int selection = -1;
            while (selection == -1) {
                if(Thread.currentThread().isInterrupted()){
                    return;
                }
                if(sc.hasNextInt()){
                    selection = sc.nextInt();
                    onSelected.accept(selection);
                } else {
                    sc.next();
                    selection = -1;
                }
            }
        });
        getInstance().thread.start();
    }

    /**
     * Reset input stream (?)
     */
    public static void closeInput(){
        try {
            getInstance().thread.interrupt();
        } catch(NullPointerException e){
            // do nothing
        }
    }
}

Задача

Этот код крайне ненадежен. Я покажу вам, что я имею в виду через мгновение. Я сделал игрушечный класс под названием Клиент и в main я смоделировал доход TimerEnd с помощью таймера.

class Client {
    /**
     * Ask user to type a number and send it to the server
     */
    void makeRequest(){
        InputManager.closeInput();
        InputManager.ask((selected) -> {
            System.out.println("Sent message: " + selected);
        });
    }

    public static void main(String[] args) {
        Client client = new Client();

        client.makeRequest();

        // Simulate Server messages
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Message received");
                client.makeRequest();
            }
        }, 5000, 5000);
    }
}

Вот как это работает в действии:

Type a number:
2
Sent message: 2
Message received
Type a number:
3
Sent message: 3
Message received
Type a number:     // Here I don't type anything
Message received
Type a number:
Message received
Type a number:
Message received
Type a number:     // Here I can send multiple messages on the same "turn"
1
Sent message: 1
2
Message received

Необразованное предположение

В настоящее время я предполагаю, что Scanner продолжает ожидать ввода, и поэтому оператор if(isInterrupted) не выполняется, пока не будет введен ввод. Если так, как я могу избежать этого поведения?

Я понимаю, что этот вопрос чрезвычайно (и, возможно, излишне) длинный, и, поскольку вы его прочитали, позвольте мне поблагодарить вас за то, что вы нашли время.

Минимальный, полный и проверяемый код

package com.company;

import java.util.*;
import java.util.function.Consumer;



class InputManager{
    private Thread thread;
    private InputManager(){}
    private static InputManager instance;
    private static InputManager getInstance(){
        if(instance == null){
            instance = new InputManager();
        }
        return instance;
    }

    /**
     * Ask user to type a number.
     * @param onSelected When the user has made his choice, this callback will be executed
     */
    public static void ask( Consumer<Integer> onSelected){
        getInstance().thread = new Thread(() -> {
            System.out.println("Type a number:");

            Scanner sc = new Scanner(System.in);
            int selection = -1;
            while (selection == -1) {
                if(Thread.currentThread().isInterrupted()){
                    return;
                }
                if(sc.hasNextInt()){
                    selection = sc.nextInt();
                    onSelected.accept(selection);
                } else {
                    sc.next();
                    selection = -1;
                }
            }
        });
        getInstance().thread.start();
    }

    /**
     * Reset input stream (?)
     */
    public static void closeInput(){
        try {
            getInstance().thread.interrupt();
        } catch(NullPointerException e){
            // do nothing
        }
    }
}

class Client {
    /**
     * Ask user to type a number and send it to the server
     */
    void makeRequest(){
        InputManager.closeInput();
        InputManager.ask((selected) -> {
            System.out.println("Sent message: " + selected);
        });
    }

    public static void main(String[] args) {
        Client client = new Client();

        client.makeRequest();
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Message received: thread interrupted");
                client.makeRequest();
            }
        }, 5000, 5000);
    }
}

Ответы [ 2 ]

0 голосов
/ 29 июня 2018

На мой взгляд, вы можете использовать 3 типа потоков:

  1. Основной поток переключается между пользователями, объявляет игроков для игры, проверяет условия выигрыша и запускает таймер на каждом ходу.
  2. Второй поток постоянно читает вводимые пользователем данные. После прочтения ввода пользователя, он уведомляет основной поток.
  3. Наконец поток ждет 5 секунд, а затем уведомляет основной поток.

Поэтому я буду использовать 2 производителей и 1 потребителя следующим образом:

  1. Производитель, который "производит" отсканированный пользовательский ввод (передает его Потребителю).
  2. Производитель, который "производит" события тайм-аута (которые уведомляют Потребителя).
  3. Потребитель, который переключается между игроками и запускает производителей.

Все это, так что вам не придется возиться с прерыванием какого-либо работающего потока, и нет необходимости проверять, готов ли Сканер

import java.util.Scanner;

public class Main {
    private static final Scanner SCAN = new Scanner(System.in);

    //This is the Scanner's input Producer:
    private static class UserInputProducer extends Thread {
        private final UserInputConsumer uInConsumer;

        public UserInputProducer(final UserInputConsumer uInConsumer) {
            this.uInConsumer = uInConsumer;
        }

        @Override
        public void run() {
            while (true) {
                final int input = SCAN.nextInt();
                SCAN.nextLine(); //Ignore the new line character.
                uInConsumer.userInput(input); //Fire user input event (for the current user).
            }
        }
    }

    //This is the time out event Producer:
    private static class TimeOutEventProducer {
        private final UserInputConsumer uInConsumer;

        private int validReportId = Integer.MIN_VALUE; //IDs starting from Integer.MIN_VALUE and
        //going step by step to Integer.MAX_VALUE, which means about 4 billion resets can be done
        //to this Producer before an unhandled overflow occurs.

        public TimeOutEventProducer(final UserInputConsumer uInConsumer) {
            this.uInConsumer = uInConsumer;
        }

        public synchronized void reset() {
            new TimerOnce(this, ++validReportId).start(); //Start a new TimerOnce. Could be javax.swing.Timer with "setRepeats(false)".
        }

        /*sleepDone(...) is called by ALL TimerOnce objects... So we need an up-to-date id (the
        reportId) to verify that the LAST one TimerOnce finished, rather than any other.*/
        public synchronized void sleepDone(final int reportId) {
            if (reportId == validReportId) //Only the last one timeout is valid...
                uInConsumer.timedOut(); //Fire time out event (for the current user).
        }
    }

    //This is just a "Timer" object which blocks for 5 seconds:
    private static class TimerOnce extends Thread {
        private final TimeOutEventProducer timeout;
        private final int reportId;

        public TimerOnce(final TimeOutEventProducer timeout,
                         final int reportId) {
            this.timeout = timeout;
            this.reportId = reportId;
        }

        @Override
        public void run() {
            try { Thread.sleep(5000); } catch (final InterruptedException ie) {} //Wait.
            timeout.sleepDone(reportId); //Report that the time elapsed...
        }
    }

    //This is the Consumer:
    private static class UserInputConsumer {
        private final String[] names;
        private int input;
        private boolean timedOut, hasInput;

        public UserInputConsumer(final String[] names) {
            this.names = names;
        }

        public synchronized int play() {
            new UserInputProducer(this).start(); //Start scanning any user's input...
            final TimeOutEventProducer timeout = new TimeOutEventProducer(this);
            int i = -1;
            do {
                i = (i + 1) % names.length;
                hasInput = false;
                timedOut = false;
                timeout.reset(); //Start the input wait timer...
                System.out.print("User " + names[i] + " enter a number: "); //Clarify who's player is the turn.
                while (!hasInput && !timedOut)
                    try { wait(); } catch (final InterruptedException ie) {} //Wait for user input or timeout.

                //Interpret notification event (either user input, either timeout):
                if (timedOut)
                    System.out.println("Sorry, out of time.");
                else if (!hasInput)
                    throw new UnsupportedOperationException("Probably messed with the flags in the while-condition.");
            }
            while (input != 5); //Here you test the win/loss condition.
            //Lets say, for example, the user that enters number '5' wins...

            return i; //Return the winner's index.
        }

        public synchronized void timedOut() {
            timedOut = true;
            notify();
        }

        public synchronized void userInput(final int input) {
            this.input = input;
            hasInput = true;
            notify();
        }
    }

    public static void main(final String[] args) {
        System.out.print("Enter number of players: ");
        final int numPlayers = SCAN.nextInt();
        SCAN.nextLine(); //Ignore the new line character.
        final String[] names = new String[numPlayers];
        for (int i=0; i<names.length; ++i) {
            System.out.print("User " + (i+1) + " enter your name: ");
            names[i] = SCAN.nextLine();
        }

        //Start the consumer (which in turn starts the producers) and start the main logic:
        System.out.println(names[new UserInputConsumer(names).play()] + " wins!");
    }
}

Обратите внимание, что программа никогда не завершается, поскольку сканирование бесконечно. Но вы можете изменить это поведение, связавшись с while (true) условием UserInputProducer.

0 голосов
/ 29 июня 2018

Хорошо, я разработал решение. Как я и думал, проблема в том, что цикл while (конечно) блокировался в Scanner.hasNext. Чтобы избежать блокировки, я использовал BufferedReader, который имеет эту удобную функцию, ready, которая возвращает true всякий раз, когда в System.in.

вводится новая строка.

По сути, я изменил метод InputManager.ask на:

void ask(Consumer<Integer> onSelected){
    getInstance().thread = new Thread(() -> {
        System.out.println("Type a number:");

        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        Scanner sc = new Scanner(reader);
        int selection = -1;
        try {
            while (selection == -1) {
                //While there is no input to be processed
                while (!reader.ready()) {
                    //This lets throw an InterruptedException
                    Thread.sleep(100);
                }
                if (sc.hasNextInt()) {
                    selection = sc.nextInt();
                    onSelected.accept(selection);
                } else {
                    sc.next();
                    selection = -1;
                }
            }
        } catch (IOException | InterruptedException e) {
            // do nothing: function ends
        }
    });
    getInstance().thread.start();
}

Я также добавил этот (крайне некрасивый) фрагмент кода, чтобы использовать любой ввод перед сбросом, чтобы предотвратить обнаружение любой предыдущей строки как набранной сейчас (в основном, очистка последней строки). (Если у кого-нибудь есть предложение о том, как это можно сделать более изящным способом, я более чем рад выслушать ваши мысли)

public static void closeInput(){
    try {
        BufferedReader tmp = new BufferedReader(new InputStreamReader(System.in));
        if(tmp.ready()){
            tmp.readLine();
        }
        getInstance().thread.interrupt();
    } catch(NullPointerException e){
        // do nothing
    } catch (IOException e) {
        e.printStackTrace();
    }
}
...