Skip to content

Commit 7ebd839

Browse files
KineolyanOPeyrussejmrunkle
authored
Add hangman exercise. (exercism#1758)
* WIP: start adding the exercise. * Add target implementation and some tests. * Update base class. * Complete tests. * Add more tests and correct reference implementation. * Add last tests on multiple games. * Provide many game implementation. * Update the tests, but not passing anymore. * Finalise last exercise and review difficulty. * Update the code architecture according to the level. * Correctly place the exercise in the config file. * Update the code according to exercism policies. * Remove copyright. * Update the config of the project. * Add hints in the file. * Correct indentation. Co-Authored-By: Jason Runkle <[email protected]> * Add helper methods and static factories to Output. * Comment complex test with multiple observables. * Improving the Hints, with refs to RxJava * Correct the refactoring to pass checkstyle. * Applies hints to the readme. * Correct refactoring in tests. * Use immutable collections and assertj. * Correct use of Assertj. Co-authored-by: Olivier Peyrusse <[email protected]> Co-authored-by: Jason Runkle <[email protected]>
1 parent 6b2e645 commit 7ebd839

14 files changed

Lines changed: 588 additions & 0 deletions

File tree

config.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,19 @@
14171417
"math"
14181418
]
14191419
},
1420+
{
1421+
"slug": "hangman",
1422+
"uuid": "ab3f8bf4-cfae-4f7a-b134-bb0fa4fafa63",
1423+
"core": false,
1424+
"unlocked_by": "linked-list",
1425+
"difficulty": 8,
1426+
"topics": [
1427+
"strings",
1428+
"reactive_programming",
1429+
"functional_programming"
1430+
],
1431+
"deprecated": false
1432+
},
14201433
{
14211434
"slug": "pythagorean-triplet",
14221435
"uuid": "88505f95-89e5-4a08-8ed2-208eb818cdf1",

exercises/hangman/.meta/HINTS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## Hints
2+
3+
One main aspect of Functional Programming is to have side-effect free functions, not to have to wonder that hidden objects a function has changed.
4+
5+
With Reactive Programming, instead of having a component actively looking for work, this work is pushed to the component. This is similar to callbacks, but at a much higher level, with more powerful abstractions. Very often, Reactive Programming is used in conjunction with Functional programming.
6+
7+
In the exercise, we will be using [RxJava](https://github.com/ReactiveX/RxJava), a well-known library for Reactive Programming with a Java API.
8+
9+
The simulated context of this exercise is an application receiving two inputs:
10+
11+
- the new words to guess from some game engine,
12+
- the letters chosen by the player.
13+
14+
Those two inputs are implemented with [Observables](http://reactivex.io/documentation/observable.html) - using the class [io.reactivex.Observable](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html).
15+
Basically, you can subscribe to an `Observable` to "react" to the values that are produced somewhere. For example, the game engine pushes new words when it detects a new game has started, or keyboard events generate letter inputs.
16+
But many Reactive Frameworks offer powerful abstractions, such as [`map`](http://reactivex.io/documentation/operators/map.html) that allows you to change the received input, or [`combine`](http://reactivex.io/documentation/operators/combinelatest.html) that lets you merge together one or more `Observable`s.
17+
18+
The class `Output` is the expected result of the exercise processing, allowing a front-end to
19+
display the complete state of the game without requiring from it any form of storage - thus making
20+
it functional as well.
21+
In this exercise, you have to find a way to use both inputs to generate this output in the form of an `Observable`.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import io.reactivex.Observable;
2+
3+
import java.util.*;
4+
5+
class Hangman {
6+
7+
Observable<Output> play(
8+
Observable<String> words,
9+
Observable<String> letters) {
10+
return Observable
11+
.combineLatest(
12+
words,
13+
letters.startWith(""),
14+
(word, letter) -> new AbstractMap.SimpleEntry<>(word, letter))
15+
.scan(
16+
Output.empty(),
17+
(state, entry) -> {
18+
System.out.println(state + " -> " + entry);
19+
if (state == null || state.status != Status.PLAYING) {
20+
return createNewGame(entry.getKey());
21+
} else {
22+
return processNewLetter(state, entry.getValue());
23+
}
24+
})
25+
.skip(1); // Skip the initial state
26+
}
27+
28+
private static Output createNewGame(String word) {
29+
return Output.initialState(word);
30+
}
31+
32+
private static Output processNewLetter(
33+
Output state,
34+
String letter) {
35+
if (state.isLetterAlreadyPlayed(letter)) {
36+
throw new IllegalArgumentException("Letter " + letter + " was already played");
37+
}
38+
if (state.isLetterInSecret(letter)) {
39+
return processCorrectGuess(state, letter);
40+
} else {
41+
return processIncorrectGuess(state, letter);
42+
}
43+
}
44+
45+
private static Output processCorrectGuess(Output state, String letter) {
46+
Set<String> newGuess = new HashSet<>(state.guess);
47+
newGuess.add(letter);
48+
String discovered = Output.getGuessedWord(state.secret, newGuess);
49+
Status newStatus = Output.isWin(state.secret, newGuess) ? Status.WIN : Status.PLAYING;
50+
return new Output(
51+
state.secret,
52+
discovered,
53+
newGuess,
54+
state.misses,
55+
state.parts,
56+
newStatus);
57+
}
58+
59+
private static Output processIncorrectGuess(Output state, String letter) {
60+
Set<String> newMisses = new HashSet<>(state.misses);
61+
newMisses.add(letter);
62+
List<Part> newParts = new ArrayList<>(state.parts);
63+
newParts.add(order[newParts.size()]);
64+
Status newStatus = Output.isLoss(newMisses) ? Status.LOSS : Status.PLAYING;
65+
return new Output(
66+
state.secret,
67+
state.discovered,
68+
state.guess,
69+
newMisses,
70+
newParts,
71+
newStatus);
72+
}
73+
74+
static Part[] order = Part.values();
75+
76+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import static java.util.stream.Collectors.joining;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.HashSet;
6+
import java.util.List;
7+
import java.util.Set;
8+
9+
class Output {
10+
11+
public final String secret;
12+
public final String discovered;
13+
public final Set<String> guess;
14+
public final Set<String> misses;
15+
public final List<Part> parts;
16+
public final Status status;
17+
18+
Output(
19+
final String secret,
20+
final String discovered,
21+
final Set<String> guess,
22+
final Set<String> misses,
23+
final List<Part> parts,
24+
final Status status) {
25+
this.secret = secret;
26+
this.discovered = discovered;
27+
this.guess = Set.copyOf(guess);
28+
this.misses = Set.copyOf(misses);
29+
this.parts = List.copyOf(parts);
30+
this.status = status;
31+
}
32+
33+
static Output empty() {
34+
return new Output(
35+
null,
36+
null,
37+
Collections.emptySet(),
38+
Collections.emptySet(),
39+
Collections.emptyList(),
40+
null);
41+
}
42+
43+
static Output initialState(final String secret) {
44+
return new Output(
45+
secret,
46+
getGuessedWord(secret, Collections.emptySet()),
47+
new HashSet<>(),
48+
new HashSet<>(),
49+
new ArrayList<>(),
50+
Status.PLAYING);
51+
}
52+
53+
boolean isLetterAlreadyPlayed(final String letter) {
54+
return guess.contains(letter) || misses.contains(letter);
55+
}
56+
57+
boolean isLetterInSecret(final String letter) {
58+
return secret.contains(letter);
59+
}
60+
61+
static String getGuessedWord(String secret, Set<String> letters) {
62+
return secret.chars()
63+
.mapToObj(i -> String.valueOf((char) i))
64+
.map(c -> letters.contains(c) ? c : "_")
65+
.collect(joining());
66+
}
67+
68+
static boolean isWin(String secret, Set<String> guessedLetters) {
69+
return secret.chars()
70+
.mapToObj(i -> String.valueOf((char) i))
71+
.allMatch(guessedLetters::contains);
72+
}
73+
74+
static boolean isLoss(Set<String> missedLetters) {
75+
return missedLetters.size() >= Part.values().length;
76+
}
77+
78+
@Override
79+
public String toString() {
80+
return "Output{" +
81+
"secret='" + secret + '\'' +
82+
", discovered='" + discovered + '\'' +
83+
", guess=" + guess +
84+
", misses=" + misses +
85+
", parts=" + parts +
86+
", status=" + status +
87+
'}';
88+
}
89+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
enum Part {
2+
HEAD,
3+
BODY,
4+
LEFT_ARM,
5+
RIGHT_ARM,
6+
LEFT_LEG,
7+
RIGHT_LEG
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enum Status {
2+
PLAYING,
3+
WIN,
4+
LOSS
5+
}

exercises/hangman/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Hangman
2+
3+
Implement the logic of the hangman game using functional reactive programming.
4+
5+
[Hangman][] is a simple word guessing game.
6+
7+
[Functional Reactive Programming][frp] is a way to write interactive
8+
programs. It differs from the usual perspective in that instead of
9+
saying "when the button is pressed increment the counter", you write
10+
"the value of the counter is the sum of the number of times the button
11+
is pressed."
12+
13+
Implement the basic logic behind hangman using functional reactive
14+
programming. You'll need to install an FRP library for this, this will
15+
be described in the language/track specific files of the exercise.
16+
17+
[Hangman]: https://en.wikipedia.org/wiki/Hangman_%28game%29
18+
[frp]: https://en.wikipedia.org/wiki/Functional_reactive_programming
19+
20+
# Tips
21+
22+
## Hints
23+
24+
One main aspect of Functional Programming is to have side-effect free functions, not to have to wonder that hidden objects a function has changed.
25+
26+
With Reactive Programming, instead of having a component actively looking for work, this work is pushed to the component. This is similar to callbacks, but at a much higher level, with more powerful abstractions. Very often, Reactive Programming is used in conjunction with Functional programming.
27+
28+
In the exercise, we will be using [RxJava](https://github.com/ReactiveX/RxJava), a well-known library for Reactive Programming with a Java API.
29+
30+
The simulated context of this exercise is an application receiving two inputs:
31+
32+
- the new words to guess from some game engine,
33+
- the letters chosen by the player.
34+
35+
Those two inputs are implemented with [Observables](http://reactivex.io/documentation/observable.html) - using the class [io.reactivex.Observable](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html).
36+
Basically, you can subscribe to an `Observable` to "react" to the values that are produced somewhere. For example, the game engine pushes new words when it detects a new game has started, or keyboard events generate letter inputs.
37+
But many Reactive Frameworks offer powerful abstractions, such as [`map`](http://reactivex.io/documentation/operators/map.html) that allows you to change the received input, or [`combine`](http://reactivex.io/documentation/operators/combinelatest.html) that lets you merge together one or more `Observable`s.
38+
39+
The class `Output` is the expected result of the exercise processing, allowing a front-end to
40+
display the complete state of the game without requiring from it any form of storage - thus making
41+
it functional as well.
42+
In this exercise, you have to find a way to use both inputs to generate this output in the form of an `Observable`.
43+
44+
## Setup
45+
46+
Go through the setup instructions for Java to install the necessary
47+
dependencies:
48+
49+
[https://exercism.io/tracks/java/installation](https://exercism.io/tracks/java/installation)
50+
51+
# Running the tests
52+
53+
You can run all the tests for an exercise by entering the following in your
54+
terminal:
55+
56+
```sh
57+
$ gradle test
58+
```
59+
60+
> Use `gradlew.bat` if you're on Windows
61+
62+
In the test suites all tests but the first have been skipped.
63+
64+
Once you get a test passing, you can enable the next one by removing the
65+
`@Ignore("Remove to run test")` annotation.
66+
67+
68+
## Submitting Incomplete Solutions
69+
It's possible to submit an incomplete solution so you can see how others have
70+
completed the exercise.

exercises/hangman/build.gradle

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apply plugin: "java"
2+
apply plugin: "eclipse"
3+
apply plugin: "idea"
4+
5+
repositories {
6+
mavenCentral()
7+
}
8+
9+
dependencies {
10+
compile 'io.reactivex.rxjava2:rxjava:2.2.12'
11+
12+
testCompile "junit:junit:4.13"
13+
testImplementation "org.assertj:assertj-core:3.15.0"
14+
}
15+
16+
test {
17+
testLogging {
18+
exceptionFormat = 'full'
19+
events = ["passed", "failed", "skipped"]
20+
}
21+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
3+
Since this exercise has a difficulty of > 4 it doesn't come
4+
with any starter implementation.
5+
This is so that you get to practice creating classes and methods
6+
which is an important part of programming in Java.
7+
8+
Please remove this comment when submitting your solution.
9+
10+
*/
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import static java.util.stream.Collectors.joining;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.HashSet;
6+
import java.util.List;
7+
import java.util.Set;
8+
import java.util.stream.IntStream;
9+
10+
class Output {
11+
12+
public final String secret;
13+
public final String discovered;
14+
public final Set<String> guess;
15+
public final Set<String> misses;
16+
public final List<Part> parts;
17+
public final Status status;
18+
19+
Output(
20+
final String secret,
21+
final String discovered,
22+
final Set<String> guess,
23+
final Set<String> misses,
24+
final List<Part> parts,
25+
final Status status) {
26+
this.secret = secret;
27+
this.discovered = discovered;
28+
this.guess = Set.copyOf(guess);
29+
this.misses = Set.copyOf(misses);
30+
this.parts = List.copyOf(parts);
31+
this.status = status;
32+
}
33+
34+
static Output empty() {
35+
return new Output(
36+
null,
37+
null,
38+
Collections.emptySet(),
39+
Collections.emptySet(),
40+
Collections.emptyList(),
41+
null);
42+
}
43+
44+
}

0 commit comments

Comments
 (0)