User Bohemian on stackoverflow responded to the Hangman code in my previous post with this comment:
Just FYI, you can do the whole thing in a just a few lines of code, and the main logic in two lines.
He wasn’t joking. Here’s his take:
import java.util.Random; import java.util.Scanner; /** <P>{@code java HangmanByBohemian}</P> **/ public class HangmanByBohemian { static int MAX_MISSES = 5; static String[] WORDS = { "QUICK", "BROWN", "JUMPS" }; // etc public static void main(String[] args) throws Exception { String word = WORDS[new Random().nextInt(WORDS.length)], guesses = " "; int misses = -1; for (Scanner in = new Scanner(System.in); !word.matches("[" + guesses + "]+") & (misses += word.contains(guesses.substring(0, 1)) ? 0 : 1) <= MAX_MISSES; guesses = in.nextLine().toUpperCase().charAt(0) + guesses) System.out.println(word.replaceAll("(?<=.)", " ").replaceAll("[^" + guesses + "]", "_")); System.out.println(word + (misses > MAX_MISSES ? " not" : "") + " solved with " + misses + " incorrect guesses"); } }
That’s 110 lines shorter. (Post continues below output.)
Success output (condensed a bit)
[C:\java_code\]java HangmanByBohemian
_ _ _ _ _ a
_ _ _ _ _ b
_ _ _ _ _ c
_ _ _ C _ d
_ _ _ C _ k
_ _ _ C K q
Q _ _ C K a
Q _ _ C K u
Q U _ C K i
Q U I C K solved with 4 incorrect guesses
Failure output
[C:\java_code\]java HangmanByBohemian
_ _ _ _ _ a
_ _ _ _ _ b
B _ _ _ _ c
B _ _ _ _ d
B _ _ _ _ e
B _ _ _ _ r
B R _ _ _ n
B R _ _ N x
B R _ _ N y
B R O W N not solved with 6 incorrect guesses
I’ve taken this dense-and-great answer, and made it my own. It’s not better, I did this only for the sake of learning it. While this doesn’t fulfill the specifications as originally asked, the specifications are very different than normal hangman (you have to explicitly specify indexes at which the guessed-letter applies to). This application is much closer to normal Hangman rules.
The major differences, as compared to Bohemian’s, are:
- It’s a
do-while
, instead of afor
loop. - It only keeps unique characters, and compares against a separate
currCharGuessAsStr
, instead of the first character inallGuessCharsStr
. - It notifies the user of bad input (no characters) instead of crashing, and requires input to be a single letter.
- It displays state at each step.
Pre loop
public class Hangman { static int MAX_BAD_GUESSES = 10; static String[] WORDS = { "QUICK", "BROWN", "JUMPS" }; // etc public static void main(String[] args) throws Exception { String word = WORDS[new Random().nextInt(WORDS.length)]; Scanner in = new Scanner(System.in); int badGuessCount = 0; String guessInput = null; //This is keyed by a one-character STRING instead of an //actual character. This is to avoid translating it back //and forth between string and char. Map<String,Object> unqGuessedCharsAsStrMap = new TreeMap<String,Object>(); //Must be initialized to the empty string. Initializing to null //Will result in the literal string "null" being added to it. String allGuessCharsStr = ""; String currCharGuessAsStr = " ";
The loop
do { System.out.print(getMasked(word, allGuessCharsStr) + " Guess a character [Bad guesses: " + badGuessCount + " of " + MAX_BAD_GUESSES + "]: "); guessInput = in.nextLine(); if(guessInput != null) { //null on first iteration only if(!guessInput.matches("^[a-zA-Z]$")) { System.out.println("Bad input. Must a single letter."); badGuessCount++; } else { //Definitely valid input, and exactly one character currCharGuessAsStr = guessInput.toUpperCase(); if(unqGuessedCharsAsStrMap.containsKey(currCharGuessAsStr)) { //Trim to eliminate initialization space System.out.println("Already guessed that letter. All guesses: " + allGuessCharsStr.trim()); } else { unqGuessedCharsAsStrMap.put(currCharGuessAsStr, null); //Prepend just-guessed character and sort it. allGuessCharsStr += currCharGuessAsStr; char[] allGuessedChars = allGuessCharsStr.toCharArray(); Arrays.sort(allGuessedChars); allGuessCharsStr = new String(allGuessedChars); } if(!word.contains(currCharGuessAsStr)) { badGuessCount++; } } } } while(!word.matches("[" + allGuessCharsStr + "]+") && badGuessCount <= MAX_BAD_GUESSES);
Post-loop, and getMasked(s,s)
function
System.out.println(getMasked(word, allGuessCharsStr)); System.out.println(word + (badGuessCount > MAX_BAD_GUESSES ? " not" : "") + " solved with " + badGuessCount + " incorrect guesses (max allowed=" + MAX_BAD_GUESSES + ")."); } /** @param all_guessesStrESIfNone May not be null. If empty, no guesses have been made yet. @exception PatternSyntaxException If all_guessCharsStr is empty. **/ private static final String getMasked(String secret_word, String all_guessesStrESIfNone) { try { if(all_guessesStrESIfNone.length() == 0) { all_guessesStrESIfNone = " "; //Any non-letter will suffice } } catch(NullPointerException npx) { throw new NullPointerException("all_guessesStrESIfNone"); } //Mask all not-yet-guessed characters with an underscore. secret_word = secret_word.replaceAll("[^" + all_guessesStrESIfNone + "]", "_"); //Insert a space between every character (trim eliminates the final). return secret_word.replaceAll("(.)", "$1 ").trim(); } }
Solved output
[R:\jeffy\programming\sandbox\xbnjava]java Hangman
_ _ _ _ _ Guess a character [Bad guesses: 0 of 10]: a
_ _ _ _ _ Guess a character [Bad guesses: 1 of 10]: b
_ _ _ _ _ Guess a character [Bad guesses: 2 of 10]: c
_ _ _ _ _ Guess a character [Bad guesses: 3 of 10]: d
_ _ _ _ _ Guess a character [Bad guesses: 4 of 10]: e
_ _ _ _ _ Guess a character [Bad guesses: 5 of 10]: f
_ _ _ _ _ Guess a character [Bad guesses: 6 of 10]: j
J _ _ _ _ Guess a character [Bad guesses: 6 of 10]: q
J _ _ _ _ Guess a character [Bad guesses: 7 of 10]: m
J _ M _ _ Guess a character [Bad guesses: 7 of 10]: p
J _ M P _ Guess a character [Bad guesses: 7 of 10]: u
J U M P _ Guess a character [Bad guesses: 7 of 10]: s
J U M P S
JUMPS solved with 7 incorrect guesses (max allowed=10).
Unsolved output
[R:\jeffy\programming\sandbox\xbnjava]java Hangman
_ _ _ _ _ Guess a character [Bad guesses: 0 of 10]: a
_ _ _ _ _ Guess a character [Bad guesses: 1 of 10]: b
_ _ _ _ _ Guess a character [Bad guesses: 2 of 10]: c
_ _ _ C _ Guess a character [Bad guesses: 2 of 10]: d
_ _ _ C _ Guess a character [Bad guesses: 3 of 10]: e
_ _ _ C _ Guess a character [Bad guesses: 4 of 10]: f
_ _ _ C _ Guess a character [Bad guesses: 5 of 10]: j
_ _ _ C _ Guess a character [Bad guesses: 6 of 10]: e
Already guessed that letter. All guesses: ABCDEFJ
_ _ _ C _ Guess a character [Bad guesses: 7 of 10]:
Bad input. Must a single letter.
_ _ _ C _ Guess a character [Bad guesses: 8 of 10]: tt
Bad input. Must a single letter.
_ _ _ C _ Guess a character [Bad guesses: 9 of 10]: q
Q _ _ C _ Guess a character [Bad guesses: 9 of 10]: k
Q _ _ C K Guess a character [Bad guesses: 9 of 10]: l
Q _ _ C K Guess a character [Bad guesses: 10 of 10]: z
Q _ _ C K
QUICK not solved with 11 incorrect guesses (max allowed=10).
Full code
import java.util.Arrays; import java.util.Map; import java.util.TreeMap; import java.util.Random; import java.util.Scanner; /** <P>{@code java Hangman}</P> **/ public class Hangman { static int MAX_BAD_GUESSES = 10; static String[] WORDS = { "QUICK", "BROWN", "JUMPS" }; // etc public static void main(String[] args) throws Exception { String word = WORDS[new Random().nextInt(WORDS.length)]; Scanner in = new Scanner(System.in); int badGuessCount = 0; String guessInput = null; //This is keyed by a one-character STRING instead of an //actual character. This is to avoid translating it back //and forth between string and char. Map<String,Object> unqGuessedCharsAsStrMap = new TreeMap<String,Object>(); //Must be initialized to the empty string. Initializing to null //Will result in the literal string "null" (those four characters, //sorted among all the guessed characters) being added to it. String allGuessCharsStr = ""; String currCharGuessAsStr = " "; do { System.out.print(getMasked(word, allGuessCharsStr) + " Guess a character [Bad guesses: " + badGuessCount + " of " + MAX_BAD_GUESSES + "]: "); guessInput = in.nextLine(); if(guessInput != null) { //null on first iteration only if(!guessInput.matches("^[a-zA-Z]$")) { System.out.println("Bad input. Must a single letter."); badGuessCount++; } else { //Definitely valid input, and exactly one character currCharGuessAsStr = guessInput.toUpperCase(); if(unqGuessedCharsAsStrMap.containsKey(currCharGuessAsStr)) { //Trim to eliminate initialization space System.out.println("Already guessed that letter. All guesses: " + allGuessCharsStr.trim()); } else { unqGuessedCharsAsStrMap.put(currCharGuessAsStr, null); //Prepend just-guessed character and sort it. allGuessCharsStr += currCharGuessAsStr; char[] allGuessedChars = allGuessCharsStr.toCharArray(); Arrays.sort(allGuessedChars); allGuessCharsStr = new String(allGuessedChars); } if(!word.contains(currCharGuessAsStr)) { badGuessCount++; } } } } while(!word.matches("[" + allGuessCharsStr + "]+") && badGuessCount <= MAX_BAD_GUESSES); System.out.println(getMasked(word, allGuessCharsStr)); System.out.println(word + (badGuessCount > MAX_BAD_GUESSES ? " not" : "") + " solved with " + badGuessCount + " incorrect guesses (max allowed=" + MAX_BAD_GUESSES + ")."); } /** @param all_guessesStrESIfNone May not be null. If empty, no guesses have been made yet. @exception PatternSyntaxException If all_guessCharsStr is empty. **/ private static final String getMasked(String secret_word, String all_guessesStrESIfNone) { try { if(all_guessesStrESIfNone.length() == 0) { all_guessesStrESIfNone = " "; //Any non-letter will suffice } } catch(NullPointerException npx) { throw new NullPointerException("all_guessesStrESIfNone"); } //Mask all not-yet-guessed characters with an underscore. secret_word = secret_word.replaceAll("[^" + all_guessesStrESIfNone + "]", "_"); //Insert a space between every character (trim eliminates the final). return secret_word.replaceAll("(.)", "$1 ").trim(); } }