diff --git a/dialog/src/main/java/roboy/dialog/states/gameStates/ChooseGameState.java b/dialog/src/main/java/roboy/dialog/states/gameStates/ChooseGameState.java index 26da88af..06bdeed7 100644 --- a/dialog/src/main/java/roboy/dialog/states/gameStates/ChooseGameState.java +++ b/dialog/src/main/java/roboy/dialog/states/gameStates/ChooseGameState.java @@ -2,96 +2,183 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import roboy.dialog.Segue; import roboy.dialog.states.definitions.State; import roboy.dialog.states.definitions.StateParameters; import roboy.linguistics.Linguistics; import roboy.linguistics.sentenceanalysis.Interpretation; import roboy.talk.PhraseCollection; import roboy.talk.Verbalizer; +import roboy.util.Maps; import roboy.util.RandomList; -import java.util.Arrays; + +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; public class ChooseGameState extends State { - private final static String TRANSITION_CHOSE_SNAPCHAT = "choseSnapchat"; - private final static String TRANSITION_CHOSE_20_Q = "chose20questions"; + + /* + Hey, if you want to add a new GameState, you will need to add your GameState into the HashMap EXISTING_GAME_MAP. That should be it. Don't forget to increment the initialCapacity of the HashMap + */ + private final static String TRANSITION_EXIT = "exitGame"; - private final static RandomList EXISTING_GAMES = new RandomList<>(Arrays.asList("Snapchat", "Akinator")); + private final static HashMap EXISTING_GAME_MAP = new HashMap<>(2); //CHANGE THE VALUE OF THIS TO THE AMOUNT OF GAMES YOU SHALL ADD (EFFICIENCY) private final Logger LOGGER = LogManager.getLogger(); private String game = null; private String suggestedGame = null; + public ChooseGameState(String stateIdentifier, StateParameters params) { super(stateIdentifier, params); } + //Can't fill this at constructor or static constructor level, so it is done in act() + private void fillExistingGameMap(){ + if(EXISTING_GAME_MAP.isEmpty()){ + addElement("Akinator", (GamingTwentyQuestionsState .transitionName)); + addElement("Snapchat", (GamingSnapchatState .transitionName)); + } + } + + private void addElement(String key, String transitionName){ + if(getAllTransitions().containsKey(transitionName)){ + EXISTING_GAME_MAP.put(key, (GameState) getTransition(transitionName)); + } + else{ + LOGGER.error("Cannot add "+transitionName+" to EXISTING_GAME_MAP because transition does not exist"); + } + } @Override public Output act() { - do { - suggestedGame = EXISTING_GAMES.getRandomElement(); + fillExistingGameMap(); + + //Pick a game that is playable + RandomList randomList = new RandomList<>(); + randomList.addAll( + //Streams to Filter, because why have Java 1.8 if we can't make use of it + //Basically: Get all keys, turn that list into a stream, filter the stream by whether the key's value (the game) is capable of starting. Then put all those who can start into a random list. + EXISTING_GAME_MAP.keySet().stream().filter(s -> EXISTING_GAME_MAP.get(s).canStartGame()) + .collect(Collectors.toCollection(ArrayList::new))); + + //If no games are playable + if(randomList.isEmpty()){ + LOGGER.info("No games available"); + + suggestedGame = "exit"; + return Output.say("I do not think I know any games that are playable in this environment. Should I try again?"); + } + //Choose a random game that is playable + else{ + LOGGER.debug(""+randomList.size()+" games are available to play"); + suggestedGame = randomList.getRandomElement(); + return Output.say(String.format(PhraseCollection.GAME_ASKING_PHRASES.getRandomElement(), suggestedGame)); } - while(getRosMainNode() == null && suggestedGame == "Snapchat"); - - return Output.say(String.format(PhraseCollection.GAME_ASKING_PHRASES.getRandomElement(), suggestedGame)); } @Override public Output react(Interpretation input) { + //Reset the Value of Game + game = ""; + Linguistics.UtteranceSentiment inputSentiment = getInference().inferSentiment(input); String inputGame = inferGame(input); - if (inputSentiment == Linguistics.UtteranceSentiment.POSITIVE){ - game = suggestedGame; - return Output.say(Verbalizer.startSomething.getRandomElement()); - } else if (!inputGame.isEmpty()){ - game = inputGame; - if(game.equals("Snapchat") && getRosMainNode() == null){ + //If no games can be played, should we try again? + if(suggestedGame.equals("exit")){ + if(inputSentiment == Linguistics.UtteranceSentiment.NEGATIVE){ + game = suggestedGame; + } + } + //If a game can be played... + else { + //If you AGREE with the selected game, try to play the game + if (inputSentiment == Linguistics.UtteranceSentiment.POSITIVE) { + return attemptGameLaunch(suggestedGame); + } + //If you suggested your OWN game, try to play the game + if (!inputGame.isEmpty()) { + return attemptGameLaunch(inputGame); + } + //If you DISAGREE with the selected game, exit... + if (inputSentiment == Linguistics.UtteranceSentiment.NEGATIVE) { game = "exit"; - LOGGER.info("Trying to start Snapchat Game but ROS is not initialised."); - Segue s = new Segue(Segue.SegueType.CONNECTING_PHRASE, 0.5); - return Output.say(Verbalizer.rosDisconnect.getRandomElement() + String.format("What a pitty, %s. Snapchat is not possible right now. ", - getContext().ACTIVE_INTERLOCUTOR.getValue().getName())).setSegue(s); } - return Output.say(Verbalizer.startSomething.getRandomElement()); - } else if (inputSentiment == Linguistics.UtteranceSentiment.NEGATIVE){ - game = "exit"; } return Output.sayNothing(); } + private Output attemptGameLaunch(String inputGame) { + //Check if Game is playable + if(EXISTING_GAME_MAP.get(inputGame).canStartGame()) { + //If so, lets go start it + game = inputGame; + return Output.say(Verbalizer.startSomething.getRandomElement()); + } + else{ + //Else, quit and issue a warning + LOGGER.warn("Detected that the given game cannot be played for some reason"); + + game = "exit"; + return EXISTING_GAME_MAP.get(inputGame).cannotStartWarning(); + } + } + @Override public State getNextState() { - - switch (game){ - case "Akinator": - return getTransition(TRANSITION_CHOSE_20_Q); - case "Snapchat": - return getTransition(TRANSITION_CHOSE_SNAPCHAT); - case "exit": - return getTransition(TRANSITION_EXIT); - default: + //CASE: EXIT + if(game.equals("exit")) { + return getTransition(TRANSITION_EXIT); + } + else{ + //Does game exist in Map --> Basically, should we launch a game... + if(EXISTING_GAME_MAP.containsKey(game)) { + String transition = Maps.value2Key( getAllTransitions(), EXISTING_GAME_MAP.get(game)).get(); + return getTransition(transition); + } + //Or repeat this state again, because we need to check something + else{ return this; + } } +// switch (game){ +// case "Akinator": +// return getTransition(TRANSITION_CHOSE_20_Q); +// case "Snapchat": +// return getTransition(TRANSITION_CHOSE_SNAPCHAT); +// case "exit": +// return getTransition(TRANSITION_EXIT); +// default: +// return this; +// } } private String inferGame(Interpretation input){ - + //VERY IMPORTANT: If your tags conflict, the one which comes first in the Hash Map shall be taken, the other shall be ignored. If for some reason you have conflicting Tags, rewrite this method List tokens = input.getTokens(); - game = ""; - if(tokens != null && !tokens.isEmpty()){ - if(tokens.contains("akinator") || tokens.contains("guessing") || tokens.contains("questions")){ - game = "Akinator"; - } else if (tokens.contains("snapchat") || tokens.contains("filters") || tokens.contains("filter") || tokens.contains("mask")){ - game = "Snapchat"; + String inferredGame = ""; + //If Tokens Exist + if(tokens != null && !tokens.isEmpty()) { + //Get All Keys + for (String key : EXISTING_GAME_MAP.keySet()) { + //Get all Tags from each Keys Value + for (String tag : EXISTING_GAME_MAP.get(key).getTags()) { + //If Tag is found in tokens + if (tokens.contains(tag)) { + //Return the key/game + return key; + } + } } } - return game; + return inferredGame; } + + } diff --git a/dialog/src/main/java/roboy/dialog/states/gameStates/GameState.java b/dialog/src/main/java/roboy/dialog/states/gameStates/GameState.java new file mode 100644 index 00000000..9d14fb48 --- /dev/null +++ b/dialog/src/main/java/roboy/dialog/states/gameStates/GameState.java @@ -0,0 +1,58 @@ +package roboy.dialog.states.gameStates; + +import roboy.dialog.states.definitions.State; +import roboy.dialog.states.definitions.StateParameters; +import roboy.linguistics.sentenceanalysis.Interpretation; + +import java.util.Collection; +import java.util.List; + +public abstract class GameState extends State { + + public static String transitionName; + + /** + * Create a state object with given identifier (state name) and parameters. + *

+ * The parameters should contain a reference to a state machine for later use. + * The state will not automatically add itself to the state machine. + * + * @param stateIdentifier identifier (name) of this state + * @param params parameters for this state, should contain a reference to a state machine + */ + public GameState(String stateIdentifier, StateParameters params) { + super(stateIdentifier, params); + } + + /** + * Can the game start? + * @return True if game is capable of starting + */ + public abstract boolean canStartGame(); + + /** + * What Roboy shall say to the user, explaining why he cannot start the given game, ie. Sorry, I need an internet connection to play World of Warcraft. + * @return Output that contains reasoning + */ + public abstract Output cannotStartWarning(); + + /** + * Tags that shall be used to infer, whether or not the user is specifically asking for a game. See {@link ChooseGameState}.infer method for how this is specifically implemented. + * @return Collection of tags + */ + public abstract Collection getTags(); + + + + public boolean checkUserSaidStop(Interpretation input){ + boolean stopGame = false; + List tokens = input.getTokens(); + if(tokens != null && !tokens.isEmpty()){ + if(tokens.contains("boring") || tokens.contains("stop") || tokens.contains("bored")){ + stopGame = true; + + } + } + return stopGame; + } +} diff --git a/dialog/src/main/java/roboy/dialog/states/gameStates/GamingSnapchatState.java b/dialog/src/main/java/roboy/dialog/states/gameStates/GamingSnapchatState.java index 3ab956fe..214ea599 100644 --- a/dialog/src/main/java/roboy/dialog/states/gameStates/GamingSnapchatState.java +++ b/dialog/src/main/java/roboy/dialog/states/gameStates/GamingSnapchatState.java @@ -2,12 +2,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import roboy.dialog.Segue; import roboy.dialog.states.definitions.State; import roboy.dialog.states.definitions.StateParameters; import roboy.linguistics.Linguistics; import roboy.linguistics.sentenceanalysis.Interpretation; import roboy.logic.Inference; import roboy.talk.PhraseCollection; +import roboy.talk.Verbalizer; import roboy.util.RandomList; import java.util.*; @@ -15,7 +17,7 @@ import static roboy.util.FileLineReader.readFile; -public class GamingSnapchatState extends State { +public class GamingSnapchatState extends GameState { private final static String TRANSITION_GAME_ENDED = "gameEnded"; private final static String EXISTING_FILTERS_ID = "filterFile"; @@ -33,6 +35,8 @@ public class GamingSnapchatState extends State { private String suggestedFilter = ""; + public static String transitionName = "choseSnapchat"; + public GamingSnapchatState(String stateIdentifier, StateParameters params) { super(stateIdentifier, params); @@ -54,7 +58,8 @@ public Output react(Interpretation input) { Linguistics.UtteranceSentiment inputSentiment = getInference().inferSentiment(input); List inputFilters = localInference.inferSnapchatFilter(input, EXISTING_FILTERS); - if(!checkUserSaidStop(input)) { + stopGame = checkUserSaidStop(input); + if(!stopGame) { if (inputSentiment == Linguistics.UtteranceSentiment.POSITIVE) { desiredFilters.add(suggestedFilter); } else if (inputFilters != null) { @@ -79,19 +84,6 @@ public State getNextState() { } } - private boolean checkUserSaidStop(Interpretation input){ - - stopGame = false; - List tokens = input.getTokens(); - if(tokens != null && !tokens.isEmpty()){ - if(tokens.contains("boring") || tokens.contains("stop") || tokens.contains("bored")){ - stopGame = true; - - } - } - return stopGame; - } - private Map> buildSynonymMap(List filters){ Map> filterMap = new HashMap<>(); @@ -121,4 +113,21 @@ private Map> buildSynonymMap(List filters){ return filterMap; } + @Override + public boolean canStartGame(){ + return getRosMainNode() != null; + } + + @Override + public Output cannotStartWarning() { + Segue s = new Segue(Segue.SegueType.CONNECTING_PHRASE, 0.5); + return Output.say(Verbalizer.rosDisconnect.getRandomElement() + String.format("What a pity, %s. Snapchat is not possible right now.", + getContext().ACTIVE_INTERLOCUTOR.getValue().getName())).setSegue(s); + } + + @Override + public Collection getTags(){ + return Arrays.asList("snapchat", "filters", "filter"); + } + } diff --git a/dialog/src/main/java/roboy/dialog/states/gameStates/GamingTwentyQuestionsState.java b/dialog/src/main/java/roboy/dialog/states/gameStates/GamingTwentyQuestionsState.java index 6bb45962..d6ad24a4 100644 --- a/dialog/src/main/java/roboy/dialog/states/gameStates/GamingTwentyQuestionsState.java +++ b/dialog/src/main/java/roboy/dialog/states/gameStates/GamingTwentyQuestionsState.java @@ -1,40 +1,35 @@ package roboy.dialog.states.gameStates; -import com.ibm.watson.developer_cloud.alchemy.v1.model.SAORelation; import com.markozajc.akiwrapper.Akiwrapper; -import com.markozajc.akiwrapper.AkiwrapperBuilder; -import com.markozajc.akiwrapper.core.entities.Question; import com.markozajc.akiwrapper.Akiwrapper.Answer; +import com.markozajc.akiwrapper.AkiwrapperBuilder; import com.markozajc.akiwrapper.core.entities.Guess; +import com.markozajc.akiwrapper.core.entities.Question; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import roboy.context.Context; import roboy.dialog.Segue; import roboy.dialog.states.definitions.State; import roboy.dialog.states.definitions.StateParameters; import roboy.linguistics.Linguistics; import roboy.linguistics.sentenceanalysis.Interpretation; -import roboy.memory.nodes.Interlocutor; -import roboy.ros.RosMainNode; import roboy.talk.PhraseCollection; import roboy.talk.Verbalizer; -import roboy.util.RandomList; +import roboy.util.NetworkUtils; import java.io.IOException; -import java.lang.ref.PhantomReference; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; -public class GamingTwentyQuestionsState extends State { - +public class GamingTwentyQuestionsState extends GameState { private static final double PROBABILITY_THRESHOLD = 0.6; private final static String TRANSITION_GAME_ENDED = "gameEnded"; private final Logger LOGGER = LogManager.getLogger(); - private Akiwrapper aw = new AkiwrapperBuilder().setFilterProfanity(true).build(); + private Akiwrapper aw; private Question nextQuestion = null; private Guess currentGuess = null; @@ -48,13 +43,15 @@ public class GamingTwentyQuestionsState extends State { private boolean emotionShown = false; private String winner = ""; + public static String transitionName = "chose20questions"; + public GamingTwentyQuestionsState(String stateIdentifier, StateParameters params) { super(stateIdentifier, params); } @Override public Output act() { - + setAW(false); if(!userReady){ return Output.say(PhraseCollection.AKINATOR_INTRO_PHRASES.getRandomElement()); @@ -83,7 +80,8 @@ public Output react(Interpretation input) { String intent = getIntent (input); Linguistics.UtteranceSentiment inputSentiment = getInference().inferSentiment(input); - if(checkUserSaidStop(input)){ + stopGame = checkUserSaidStop(input); + if(stopGame){ gameFinished = true; return Output.sayNothing(); @@ -253,7 +251,7 @@ private Output processUserGuessAnswer(String intent){ private void resetGame(){ - aw = new AkiwrapperBuilder().setFilterProfanity(true).build(); + setAW(true); declined.clear(); userReady = false; guessesAvailable = false; @@ -261,19 +259,6 @@ private void resetGame(){ winner = ""; } - private boolean checkUserSaidStop(Interpretation input){ - - stopGame = false; - List tokens = input.getTokens(); - if(tokens != null && !tokens.isEmpty()){ - if(tokens.contains("boring") || tokens.contains("stop") || tokens.contains("bored")){ - stopGame = true; - - } - } - return stopGame; - } - private void applyFilter(String winner){ if(winner.equals("roboy")){ @@ -286,4 +271,36 @@ private void applyFilter(String winner){ } } + + @Override + public boolean canStartGame() { + setAW(false); + return aw!=null && aw.getServer().isUp(); + } + + @Override + public Output cannotStartWarning() { + return Output.say("Sorry, I need Internet Access to play this game"); + } + + @Override + public Collection getTags(){ + return Arrays.asList("akinator", "guessing", "questions"); + } + + private void setAW(boolean force){ + if ((force || aw == null)) { + if(NetworkUtils.isInternetWorking()) { + aw = new AkiwrapperBuilder().setFilterProfanity(true).build(); + } + else{ + LOGGER.warn("No Internet Connection"); + } + } + LOGGER.debug("AW Object already exists. Was not overwritten"); + } + + + + } diff --git a/dialog/src/main/java/roboy/util/Maps.java b/dialog/src/main/java/roboy/util/Maps.java index b072d132..43deb875 100755 --- a/dialog/src/main/java/roboy/util/Maps.java +++ b/dialog/src/main/java/roboy/util/Maps.java @@ -1,7 +1,9 @@ package roboy.util; -import java.util.HashMap; -import java.util.Map; +import com.github.jsonldjava.utils.Obj; + +import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; /** * Helper class for map related tasks. @@ -31,4 +33,22 @@ public static Map intStringMap(Object... elements){ } return result; } + + //The day when Java finally supports Bidirectional Maps... + public static Collection value2Keys(HashMap map, values value){ + ArrayList collection = new ArrayList<>(); + for(keys k : map.keySet()){ + if(map.get(k).equals(value)){ + collection.add(k); + } + } + return collection; + } + public static Optional value2Key(HashMap map, values value){ + Collection v2k = value2Keys(map, value); + if(v2k==null || v2k.isEmpty() || v2k.size()>=2){ + return Optional.empty(); + } + else return v2k.stream().findFirst(); + } } diff --git a/dialog/src/main/java/roboy/util/NetworkUtils.java b/dialog/src/main/java/roboy/util/NetworkUtils.java new file mode 100644 index 00000000..98f059fa --- /dev/null +++ b/dialog/src/main/java/roboy/util/NetworkUtils.java @@ -0,0 +1,44 @@ +package roboy.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +public class NetworkUtils { + private static final int TIMEOUT = 1000; + private static final Logger LOGGER = LogManager.getLogger(); + + public static boolean isInternetWorking(String addr){ + return isReachable(addr, TIMEOUT); + } + + /** + * Pings Cloudfare, Google and OpenDNS servers + */ + public static boolean isInternetWorking(){ + boolean cloudFlare = isInternetWorking("1.1.1.1"), google = isInternetWorking("172.217.22.46"), openDNS = isInternetWorking("146.112.62.105"); + if(cloudFlare && google && openDNS){ + LOGGER.debug("Internet working"); + return true; + } + else{ + LOGGER.warn(String.format("Cloudflare %s, Google %s, OpenDNS %s", cloudFlare, google, openDNS)); + return false; + } + } + //Shamelessly stolen from stackoverflow + //Credits: https://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address + public static boolean isReachable(String addr, int timeOutMillis) { + try { + try (Socket soc = new Socket()) { + soc.connect(new InetSocketAddress(addr, 80), timeOutMillis); + } + return true; + } catch (IOException ex) { + return false; + } + } +}