Goal
I'm currently building (to practice with java) a basic command line multiplayer turn-based game. In this game, each player has 5 seconds to make his move. When he makes his move (or when the timer ends) the other player starts his turn, etc etc.
The server sends a TimerEnded message every time the timer ends.
My current goal is to achieve flawless input reading that could be interrupted when a TimerEnded message arrives to the client.
Design
To achieve this I created a singleton called InputManager. This class handles all the input reading stuff. I created a method called ask which takes a callback as parameter. In this method I create a new thread and inside it I wait for an input with Scanner.hasNextInt.
This class has also the method closeInput which sends an Interrupt message to the thread described above.
Here's the current implementation of the class:
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
}
}
}
Problem
This code is extremely unreliable. I'll show you what I mean in just a moment.
I made a toy class called Client and in the main I simulated the TimerEnd message income with a timer.
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);
}
}
Here's how it works in action:
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
Non-educated guess
Currently, I guess that Scanner remains waiting for input and so the if(isInterrupted) statement is not hit until an input is given. If so, how can I avoid this behaviour?
I understand that this question is extremely (and maybe unnecessarily) long, and since you read it let me thank you for taking your time.
Minimal, Complete and Verifiable code
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);
}
}
Consumeris working like you expect. - markspacegetInstanceis not thread safe and it should be.Consumeris not thread safe and it should be. You're hiding aNullPointerExceptionincloseInputand you never should do that. I think over-use of lambdas is hiding problems in your code. Try re-writing it with out the lamdas and pay close attention to thread safe operations. Then we might have something we can actually comment on. - markspace