Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor hangman to harness immutability, object orientation, old&plain cli #4

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e485897
Class to encapsulate a secret phrase to be uncovered
fejnartal May 22, 2017
1f297b2
Guesses represent progressive, incremental knowledge about the solution.
May 22, 2017
cbf06b0
Modeling mistakes counter
fejnartal May 22, 2017
d9ed4eb
Hangman game coordinating class.
fejnartal May 22, 2017
f8fc70b
Random hardcoded vocabulary as in the original code.
fejnartal May 22, 2017
9b7ce20
Number of allowed mistakes should be determined by SecretPhrase
fejnartal May 23, 2017
5ac865c
Bugfix: mistake on first round were not counted.
fejnartal May 23, 2017
cd78d45
Added final touches to Hangman game class. Game status, resolution, etc
fejnartal May 23, 2017
f9a23de
Bugfix: Repeating a known letter caused it to be hidden again.
May 23, 2017
84ee240
Bugfix: Win condition was never satisfied. Expose total allowed mistakes
May 23, 2017
fe65ea1
Simple CLI interface.
fejnartal May 23, 2017
8a677ba
Delete broken test.
fejnartal May 23, 2017
8e274f1
Run Eclipse autoformatter to standardize whitespace, etc.
fejnartal May 23, 2017
c1c087c
Display total number of round at the end of the game.
fejnartal May 23, 2017
002b4d1
Changed the design of Hangman class to be fully immutable.
fejnartal May 23, 2017
4496b4a
Merge pull request #2 from yegor256/master
fejnartal May 23, 2017
facbfe2
Separate Round and Guess as two different Classes.
fejnartal May 23, 2017
8ead7b4
Merge branch 'feature/object_oriented' of https://github.com/fejnarta…
May 23, 2017
174f0ab
Polished Guess combination API & some renaming for the sake of clarity.
fejnartal May 23, 2017
cc113ff
Restructure packages to better represent relationship between classes.
fejnartal May 23, 2017
fcb7603
Remove unnecessary variable.
fejnartal May 23, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 26 additions & 78 deletions src/main/java/hangman/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,85 +14,33 @@
*/
package hangman;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.Random;
import java.util.Scanner;

public class Main {

private final InputStream input;
private final OutputStream output;
private final int max;
private static final String[] WORDS = {
"simplicity", "equality", "grandmother",
"neighborhood", "relationship", "mathematics",
"university", "explanation"
};

public Main(final InputStream in, final OutputStream out, final int m) {
this.input = in;
this.output = out;
this.max = m;
}

public static void main(final String... args) {
new Main(System.in, System.out, 5).exec();
}

public void exec() {
String word = WORDS[new Random().nextInt(WORDS.length)];
boolean[] visible = new boolean[word.length()];
int mistakes = 0;
try (final PrintStream out = new PrintStream(this.output)) {
final Iterator<String> scanner = new Scanner(this.input);
boolean done = true;
while (mistakes < this.max) {
done = true;
for (int i = 0; i < word.length(); ++i) {
if (!visible[i]) {
done = false;
}
}
if (done) {
break;
}
out.print("Guess a letter: ");
char chr = scanner.next().charAt(0);
boolean hit = false;
for (int i = 0; i < word.length(); ++i) {
if (word.charAt(i) == chr && !visible[i]) {
visible[i] = true;
hit = true;
}
}
if (hit) {
out.print("Hit!\n");
} else {
out.printf(
"Missed, mistake #%d out of %d\n",
mistakes + 1, this.max
);
++mistakes;
}
out.append("The word: ");
for (int i = 0; i < word.length(); ++i) {
if (visible[i]) {
out.print(word.charAt(i));
} else {
out.print("?");
}
}
out.append("\n\n");
}
if (done) {
out.print("You won!\n");
} else {
out.print("You lost.\n");
}
}
}
import hangman.core.Hangman;
import hangman.core.Round;
import hangman.secret.HardcodedVocabulary;

public final class Main {
public final static void main(String[] args) {
final Scanner scan = new Scanner(System.in);
final Hangman hangman = new Hangman(new HardcodedVocabulary());
Round currentRound, lastRound = null;
do {
System.out.print("Guess a letter: ");
final char c = scan.next().charAt(0);
currentRound = lastRound==null?hangman.disclose(c):hangman.discloseAlso(lastRound,c);
if (currentRound.missed) {
System.out.printf("Missed, mistake #%d out of %d\n", currentRound.mistakes, hangman.allowedMistakes);
} else {
System.out.println("Hit!");
}
System.out.println("The word: " + currentRound.currentGuess);
lastRound = currentRound;
} while(hangman.gameStage(lastRound) == Hangman.Stage.PLAYING);
if (hangman.gameStage(lastRound) == Hangman.Stage.YOUWON) {
System.out.printf("You won in %d rounds!\n", lastRound.round);
} else {
System.out.printf("You lost in %d rounds.\n", lastRound.round);
}
}
}
28 changes: 28 additions & 0 deletions src/main/java/hangman/core/Guess.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package hangman.core;

public final class Guess {
public static final char unknownChar = '\0';
public final String knownText;

public Guess( String knownText ) {
this.knownText = knownText;
}

public final boolean missed() {
return this.knownText.replace(""+unknownChar,"").length()==0;
}

public final Guess combine( Guess otherGuess ) {
String combinedSolution = "";
for( int i=0 ; i < knownText.length() ; i++ ) {
final char c = (char) (knownText.charAt(i)|otherGuess.knownText.charAt(i));
combinedSolution += c;
}
return new Guess(combinedSolution);
}

public final String toString() {
// Replace non-printable NULL characters with CLI friendly alternative
return knownText.replace(unknownChar, '_');
}
}
35 changes: 35 additions & 0 deletions src/main/java/hangman/core/Hangman.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package hangman.core;

import hangman.secret.SecretProvider;

public final class Hangman {
public static enum Stage {
PLAYING, YOUWON, YOULOSE
}

private final SecretPhrase secretPhrase;
public final int allowedMistakes;

public Hangman(SecretProvider secretProvider) {
this.secretPhrase = secretProvider.provideSecret();
this.allowedMistakes = secretPhrase.allowedMistakes;
}

public final Round disclose(char c) {
return new Round(secretPhrase.discover(c));
}

public final Round discloseAlso(Round prevRound, char c) {
return prevRound.nextRound(secretPhrase.discover(c));
}

public final Stage gameStage(Round round) {
if (round.mistakes >= secretPhrase.allowedMistakes) {
return Stage.YOULOSE;
} else if (secretPhrase.resolve(round.currentGuess)) {
return Stage.YOUWON;
} else {
return Stage.PLAYING;
}
}
}
34 changes: 34 additions & 0 deletions src/main/java/hangman/core/Round.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package hangman.core;

public final class Round {
public final int round;
public final int mistakes;
public final boolean missed;
public final Guess currentGuess;

public Round( Guess initialGuess ) {
this.round = 1;
this.currentGuess = initialGuess;
if(initialGuess.missed()) {
this.mistakes = 1;
this.missed = true;
} else {
this.mistakes = 0;
this.missed = false;
}
}

private Round( Guess guess, int newRound, int mistakes, boolean missed ) {
this.missed = missed;
this.round = newRound;
this.mistakes = mistakes;
this.currentGuess = guess;
}

public final Round nextRound( Guess newGuess ) {
if(newGuess.missed())
return new Round(currentGuess.combine(newGuess), round+1, mistakes+1, true);
else
return new Round(currentGuess.combine(newGuess), round+1, mistakes, false);
}
}
21 changes: 21 additions & 0 deletions src/main/java/hangman/core/SecretPhrase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package hangman.core;

public final class SecretPhrase {
public final int allowedMistakes;
private final String phrase;

// It seems reasonable for that the SecretPhrase
// determines the difficulty and number of allowed mistakes
public SecretPhrase(String phrase, int allowedMistakes) {
this.phrase = phrase;
this.allowedMistakes = allowedMistakes;
}

public final Guess discover(char character) {
return new Guess(phrase.replaceAll("[^" + character + "]", ""+Guess.unknownChar));
}

public final boolean resolve(Guess solution) {
return phrase.equals(solution.knownText);
}
}
17 changes: 17 additions & 0 deletions src/main/java/hangman/secret/HardcodedVocabulary.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hangman.secret;

import java.util.Random;

import hangman.core.SecretPhrase;

public final class HardcodedVocabulary implements SecretProvider {
private static final String[] WORDS = {
"simplicity", "equality", "grandmother",
"neighborhood", "relationship",
"mathematics", "university", "explanation" };

@Override
public final SecretPhrase provideSecret() {
return new SecretPhrase(WORDS[new Random().nextInt(WORDS.length)], 5);
}
}
7 changes: 7 additions & 0 deletions src/main/java/hangman/secret/SecretProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package hangman.secret;

import hangman.core.SecretPhrase;

public interface SecretProvider {
public SecretPhrase provideSecret();
}
18 changes: 18 additions & 0 deletions src/main/java/hangman/secret/StubVocabulary.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hangman.secret;

import hangman.core.SecretPhrase;

public final class StubVocabulary implements SecretProvider {
private final String secretPhrase;
private final int allowedMistakes;

public StubVocabulary(String secretPhrase, int allowedMistakes) {
this.secretPhrase = secretPhrase;
this.allowedMistakes = allowedMistakes;
}

@Override
public final SecretPhrase provideSecret() {
return new SecretPhrase(secretPhrase, allowedMistakes);
}
}
31 changes: 31 additions & 0 deletions src/test/java/hangman/GuessRoundTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package hangman;

import org.junit.Assert;
import org.junit.Test;

import hangman.core.Round;
import hangman.core.SecretPhrase;

public final class GuessRoundTest {
@Test
public final void guessRoundProgression() {
final SecretPhrase secret = new SecretPhrase( "elegant objects", 8);

final Round gr1 = new Round(secret.discover('a'));
Assert.assertEquals(1,gr1.round);
Assert.assertEquals(false,gr1.missed);
Assert.assertEquals(0,gr1.mistakes);
Assert.assertEquals("\0\0\0\0a\0\0\0\0\0\0\0\0\0\0", gr1.currentGuess.knownText);

final Round gr2 = gr1.nextRound(secret.discover('o'));
Assert.assertEquals(2,gr2.round);
Assert.assertEquals(0,gr2.mistakes);
Assert.assertEquals(false,gr2.missed);
Assert.assertEquals("\0\0\0\0a\0\0\0o\0\0\0\0\0\0", gr2.currentGuess.knownText);

final Round gr3 = gr2.nextRound(secret.discover('v'));
Assert.assertEquals(3,gr3.round);
Assert.assertEquals(1,gr3.mistakes);
Assert.assertEquals(true,gr3.missed);
}
}
24 changes: 24 additions & 0 deletions src/test/java/hangman/HagmanTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package hangman;

import org.junit.Assert;
import org.junit.Test;

import hangman.core.Hangman;
import hangman.core.Round;
import hangman.secret.StubVocabulary;

public class HagmanTest {
@Test
public void hangmanRounds() {
final Hangman hangman = new Hangman(new StubVocabulary("hangman",2));
final Round gr1 = hangman.disclose('e');
final Round gr2 = hangman.discloseAlso(gr1,'h');
final Round gr3 = hangman.discloseAlso(gr2,'n');
final Round gr4 = hangman.discloseAlso(gr3,'a');
Assert.assertEquals(4,gr4.round);
Assert.assertEquals("han\0\0an",gr4.currentGuess.knownText);
Assert.assertEquals(Hangman.Stage.PLAYING, hangman.gameStage(gr4));
final Round gr5 = hangman.discloseAlso(gr4,'w');
Assert.assertEquals(Hangman.Stage.YOULOSE, hangman.gameStage(gr5));
}
}
35 changes: 0 additions & 35 deletions src/test/java/hangman/MainTest.java

This file was deleted.

16 changes: 16 additions & 0 deletions src/test/java/hangman/SecretWordTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package hangman;

import org.junit.Assert;
import org.junit.Test;

import hangman.core.Guess;
import hangman.core.SecretPhrase;

public class SecretWordTest {
@Test
public void discoverLetters() {
final SecretPhrase secret = new SecretPhrase( "elegant objects", 8 );
final Guess partialSolution = secret.discover('e');
Assert.assertEquals("e\0e\0\0\0\0\0\0\0\0e\0\0\0", partialSolution.knownText);
}
}