diff --git a/config.json b/config.json index 14289e8920..5c938d4925 100644 --- a/config.json +++ b/config.json @@ -32,13 +32,13 @@ ] }, { - "uuid": "574d6323-5ff5-4019-9ebe-0067daafba13", "slug": "high-scores", + "uuid": "574d6323-5ff5-4019-9ebe-0067daafba13", "core": true, "unlocked_by": null, "difficulty": 1, "topics": [ - "control-flow (if-else statements)", + "control_flow_if_else_statements", "sequences", "text_formatting" ] @@ -351,7 +351,6 @@ ] }, { - "uuid": "505e7bdb-e18d-45fd-9849-0bf33492efd9", "slug": "run-length-encoding", "uuid": "505e7bdb-e18d-45fd-9849-0bf33492efd9", "core": false, @@ -1332,6 +1331,16 @@ "trees" ] }, + { + "slug": "hangman", + "uuid": "adad6be5-855d-4d61-b14a-22e468ba5b44", + "core": false, + "unlocked_by": null, + "difficulty": 5, + "topics": [ + "strings" + ] + }, { "slug": "custom-set", "uuid": "bb07c236-062c-2980-483a-a221e4724445dcd6f32", @@ -1402,15 +1411,15 @@ ] }, { - "slug": "dnd-character", - "uuid": "58625685-b5cf-4e8a-b3aa-bff54da0689d", - "core": false, - "unlocked_by": null, - "difficulty": 1, - "topics": [ - "randomness", - "integers" - ] + "slug": "dnd-character", + "uuid": "58625685-b5cf-4e8a-b3aa-bff54da0689d", + "core": false, + "unlocked_by": null, + "difficulty": 1, + "topics": [ + "integers", + "randomness" + ] }, { "slug": "retree", @@ -1432,8 +1441,8 @@ "unlocked_by": null, "difficulty": 2, "topics": [ - "conditionals", - "math" + "conditionals", + "math" ] }, { diff --git a/exercises/hangman/.meta/hints.md b/exercises/hangman/.meta/hints.md new file mode 100644 index 0000000000..4a76cf360f --- /dev/null +++ b/exercises/hangman/.meta/hints.md @@ -0,0 +1,3 @@ +## Hints + +Please ignore the part regarding FRP library, a third party library is not required for this exercise. \ No newline at end of file diff --git a/exercises/hangman/README.md b/exercises/hangman/README.md new file mode 100644 index 0000000000..225f7a7a57 --- /dev/null +++ b/exercises/hangman/README.md @@ -0,0 +1,68 @@ +# Hangman + +Implement the logic of the hangman game using functional reactive programming. + +[Hangman][] is a simple word guessing game. + +[Functional Reactive Programming][frp] is a way to write interactive +programs. It differs from the usual perspective in that instead of +saying "when the button is pressed increment the counter", you write +"the value of the counter is the sum of the number of times the button +is pressed." + +Implement the basic logic behind hangman using functional reactive +programming. You'll need to install an FRP library for this, this will +be described in the language/track specific files of the exercise. + +[Hangman]: https://en.wikipedia.org/wiki/Hangman_%28game%29 +[frp]: https://en.wikipedia.org/wiki/Functional_reactive_programming + +## Hints + +Please ignore the part regarding FRP library, a third party library is not required for this exercise. + + +## Exception messages + +Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to +indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not +every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include +a message. + +To raise a message with an exception, just write it as an argument to the exception type. For example, instead of +`raise Exception`, you should write: + +```python +raise Exception("Meaningful message indicating the source of the error") +``` + +## Running the tests + +To run the tests, run the appropriate command below ([why they are different](https://github.com/pytest-dev/pytest/issues/1629#issue-161422224)): + +- Python 2.7: `py.test hangman_test.py` +- Python 3.4+: `pytest hangman_test.py` + +Alternatively, you can tell Python to run the pytest module (allowing the same command to be used regardless of Python version): +`python -m pytest hangman_test.py` + +### Common `pytest` options + +- `-v` : enable verbose output +- `-x` : stop running tests on first failure +- `--ff` : run failures from previous test before running other test cases + +For other options, see `python -m pytest -h` + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/hangman` directory. + +You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`. + +For more detailed information about running tests, code style and linting, +please see [Running the Tests](http://exercism.io/tracks/python/tests). + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/hangman/example.py b/exercises/hangman/example.py new file mode 100644 index 0000000000..82f4db699d --- /dev/null +++ b/exercises/hangman/example.py @@ -0,0 +1,50 @@ +STATUS_WIN = "win" +STATUS_LOSE = "lose" +STATUS_ONGOING = "ongoing" + + +class Hangman: + def __init__(self, word): + self.remainingGuesses = 9 + self.status = STATUS_ONGOING + self.word = word + self.masked_word = '' + self.guesses = [] + for i in self.word: + self.masked_word += '_' + + def guess(self, char): + if self.status != STATUS_ONGOING: + raise ValueError("Game already ended, you " + self.status) + + self.update_remaining_guesses(char) + self.update_masked_word() + self.update_status() + + def update_masked_word(self): + self.masked_word = '' + for i in self.word: + if i not in self.guesses: + self.masked_word += '_' + else: + self.masked_word += i + + def update_remaining_guesses(self, char): + if char not in self.word or char in self.guesses: + self.remainingGuesses -= 1 + else: + self.guesses.append(char) + + def update_status(self): + if self.masked_word == self.word: + self.status = STATUS_WIN + elif self.remainingGuesses < 0: + self.status = STATUS_LOSE + else: + self.status = STATUS_ONGOING + + def get_masked_word(self): + return self.masked_word + + def get_status(self): + return self.status diff --git a/exercises/hangman/hangman.py b/exercises/hangman/hangman.py new file mode 100644 index 0000000000..013cd6593c --- /dev/null +++ b/exercises/hangman/hangman.py @@ -0,0 +1,20 @@ +# Game status categories +# Change the values as you see fit +STATUS_WIN = "win" +STATUS_LOSE = "lose" +STATUS_ONGOING = "ongoing" + + +class Hangman(object): + def __init__(self, word): + self.remainingGuesses = 9 + self.status = STATUS_ONGOING + + def guess(self, char): + pass + + def get_masked_word(self): + pass + + def get_status(self): + pass diff --git a/exercises/hangman/hangman_test.py b/exercises/hangman/hangman_test.py new file mode 100644 index 0000000000..26cf25ce84 --- /dev/null +++ b/exercises/hangman/hangman_test.py @@ -0,0 +1,98 @@ +import unittest + +import hangman +from hangman import Hangman + + +# Tests adapted from csharp//hangman/HangmanTest.cs + +class HangmanTests(unittest.TestCase): + def test_initially_9_failures_are_allowed(self): + game = Hangman('foo') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 9) + + def test_initially_no_letters_are_guessed(self): + game = Hangman('foo') + + self.assertEqual(game.get_masked_word(), '___') + + def test_after_10_failures_the_game_is_over(self): + game = Hangman('foo') + + for i in range(10): + game.guess('x') + + self.assertEqual(game.get_status(), hangman.STATUS_LOSE) + with self.assertRaisesWithMessage(ValueError): + game.guess('x') + + def test_feeding_a_correct_letter_removes_underscores(self): + game = Hangman('foobar') + + game.guess('b') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 9) + self.assertEqual(game.get_masked_word(), '___b__') + + game.guess('o') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 9) + self.assertEqual(game.get_masked_word(), '_oob__') + + def test_feeding_a_correct_letter_twice_counts_as_a_failure(self): + game = Hangman('foobar') + + game.guess('b') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 9) + self.assertEqual(game.get_masked_word(), '___b__') + + game.guess('b') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 8) + self.assertEqual(game.get_masked_word(), '___b__') + + def test_getting_all_the_letters_right_makes_for_a_win(self): + game = Hangman('hello') + + game.guess('b') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 8) + self.assertEqual(game.get_masked_word(), '_____') + + game.guess('e') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 8) + self.assertEqual(game.get_masked_word(), '_e___') + + game.guess('l') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 8) + self.assertEqual(game.get_masked_word(), '_ell_') + + game.guess('o') + self.assertEqual(game.get_status(), hangman.STATUS_ONGOING) + self.assertEqual(game.remainingGuesses, 8) + self.assertEqual(game.get_masked_word(), '_ello') + + game.guess('h') + self.assertEqual(game.get_status(), hangman.STATUS_WIN) + self.assertEqual(game.get_masked_word(), 'hello') + + with self.assertRaisesWithMessage(ValueError): + game.guess('x') + + # Utility functions + def setUp(self): + try: + self.assertRaisesRegex + except AttributeError: + self.assertRaisesRegex = self.assertRaisesRegexp + + def assertRaisesWithMessage(self, exception): + return self.assertRaisesRegex(exception, r".+") + + +if __name__ == '__main__': + unittest.main()