diff --git a/config.json b/config.json index 6d40440a34..7cbe1f1c73 100644 --- a/config.json +++ b/config.json @@ -906,6 +906,20 @@ "difficulty": 2, "topics": ["conditionals", "strings", "text_formatting", "transforming"] }, + { + "slug": "hangman", + "uuid": "1a7f8522-2c68-45af-a5a2-1519c03a823e", + "core": false, + "unlocked_by": "bob", + "difficulty": 4, + "topics": [ + "arrays", + "conditionals", + "loops", + "parsing", + "strings" + ] + }, { "slug": "diffie-hellman", "uuid": "127eccbd-3009-4a8f-95c1-7d8aeb608550", diff --git a/exercises/hangman/.eslintrc b/exercises/hangman/.eslintrc new file mode 100644 index 0000000000..2e5a5079a0 --- /dev/null +++ b/exercises/hangman/.eslintrc @@ -0,0 +1,26 @@ +{ + "root": true, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 7, + "sourceType": "module" + }, + "env": { + "es6": true, + "node": true, + "jest": true + }, + "extends": [ + "eslint:recommended", + "plugin:import/errors", + "plugin:import/warnings" + ], + "rules": { + "linebreak-style": "off", + + "import/extensions": "off", + "import/no-default-export": "off", + "import/no-unresolved": "off", + "import/prefer-default-export": "off" + } +} diff --git a/exercises/hangman/README.md b/exercises/hangman/README.md new file mode 100644 index 0000000000..06a6910010 --- /dev/null +++ b/exercises/hangman/README.md @@ -0,0 +1,50 @@ +# 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, optionally using functional +reactive programming. No additional library is necessary. + +[Hangman]: https://en.wikipedia.org/wiki/Hangman_%28game%29 +[frp]: https://en.wikipedia.org/wiki/Functional_reactive_programming + +## Setup + +Go through the setup instructions for Javascript to install the necessary +dependencies: + +[https://exercism.io/tracks/javascript/installation](https://exercism.io/tracks/javascript/installation) + +## Requirements + +Install assignment dependencies: + +```bash +$ npm install +``` + +## Making the test suite pass + +Execute the tests with: + +```bash +$ npm test +``` + +In the test suites all tests but the first have been skipped. + +Once you get a test passing, you can enable the next one by changing `xtest` to +`test`. + +## 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/babel.config.js b/exercises/hangman/babel.config.js new file mode 100644 index 0000000000..9da4622b24 --- /dev/null +++ b/exercises/hangman/babel.config.js @@ -0,0 +1,14 @@ +module.exports = { + presets: [ + [ + '@babel/env', + { + targets: { + node: 'current', + }, + useBuiltIns: false, + }, + + ], + ], +}; diff --git a/exercises/hangman/example.js b/exercises/hangman/example.js new file mode 100644 index 0000000000..d710a6eab2 --- /dev/null +++ b/exercises/hangman/example.js @@ -0,0 +1,60 @@ +export const GAME_STATUS = { + WIN: 'win', + LOSS: 'loss', + ONGOING: 'ongoing' +} + +export class Hangman { + constructor(word) { + this.remaining_guesses = 9 + this.status = GAME_STATUS.ONGOING + this.word = word + this.guesses = [] + this.maskedWord = '_'.repeat(this.word.length) + } + + guess(char) { + if (this.status != GAME_STATUS.ONGOING) { + throw new Error(`Game is over, status: ${this.status}.`) + } + if (this.word.split('').indexOf(char) >= 0 && this.guesses.indexOf(char) < 0) { + this.guesses.push(char) + this.updateMaskedWord() + } else { + this.remaining_guesses -= 1 + } + this.updateStatus() + } + + updateMaskedWord() { + this.maskedWord = '' + for (var i = 0; i < this.word.length; i++) { + const letter = this.word[i] + if (this.guesses.indexOf(letter) >= 0) { + this.maskedWord += letter + } else { + this.maskedWord += '_' + } + } + } + + updateStatus() { + if (this.remaining_guesses < 0) { + this.status = GAME_STATUS.LOSS + } else if (this.maskedWord === this.word) { + this.status = GAME_STATUS.WIN + } + } + + getMaskedWord() { + return this.maskedWord + } + + getStatus() { + return this.status + } + + getRemainingGuesses() { + return this.remaining_guesses + } +} diff --git a/exercises/hangman/hangman.js b/exercises/hangman/hangman.js new file mode 100644 index 0000000000..aa5faed92f --- /dev/null +++ b/exercises/hangman/hangman.js @@ -0,0 +1,32 @@ +// +// This is only a SKELETON file for the 'Scale Generator' exercise. It's been provided as a +// convenience to get you started writing code faster. +// + +export const GAME_STATUS = { + WIN: 'win', + LOSS: 'loss', + ONGOING: 'ongoing' +} + +export class Hangman { + constructor(word) { + throw new Error("Remove this statement and implement this function"); + } + + guess(char) { + throw new Error("Remove this statement and implement this function"); + } + + getMaskedWord() { + throw new Error("Remove this statement and implement this function"); + } + + getStatus() { + throw new Error("Remove this statement and implement this function"); + } + + getRemainingGuesses() { + throw new Error("Remove this statement and implement this function"); + } +} diff --git a/exercises/hangman/hangman.spec.js b/exercises/hangman/hangman.spec.js new file mode 100644 index 0000000000..5cb224517e --- /dev/null +++ b/exercises/hangman/hangman.spec.js @@ -0,0 +1,77 @@ +import { Hangman, GAME_STATUS } from './hangman'; + +describe('Hangman', () => { + test('Initially 9 failures are allowed', () => { + let game = new Hangman('foo') + expect(game.getStatus()).toEqual(GAME_STATUS.ONGOING) + expect(game.getRemainingGuesses()).toEqual(9) + }) + + xtest('Initially all letters are blanks and unguessed', () => { + let game = new Hangman('apple') + expect(game.getMaskedWord()).toEqual('_____') + }) + + xtest('After 10 failures, the game is over', () => { + let game = new Hangman('banana') + let counter = 9 + while (counter > 0) { + game.guess('z') + counter -= 1 + } + expect(game.getMaskedWord()).toEqual('______') + try { + game.guess('z') + } catch (e) { + expect(e).toBe(new Error('Game is over, status: loss.')) + } + expect(game.getMaskedWord()).toEqual('______') + expect(game.getStatus()).toEqual(GAME_STATUS.LOSS) + }) + + xtest('Entering a correct letter removes correct underscores', () => { + let game = new Hangman('orange') + game.guess('g') + expect(game.getMaskedWord()).toEqual('____g_') + game.guess('a') + expect(game.getMaskedWord()).toEqual('__a_g_') + }) + + xtest('Entering a correct letter twice counts as a failure', () => { + let game = new Hangman('pear') + game.guess('e') + expect(game.getMaskedWord()).toEqual('_e__') + expect(game.getRemainingGuesses()).toEqual(9) + game.guess('e') + expect(game.getMaskedWord()).toEqual('_e__') + expect(game.getRemainingGuesses()).toEqual(8) + }) + + xtest('Guessing all letters correctly results in a win', () => { + let game = new Hangman('peach') + game.guess('p') + expect(game.getMaskedWord()).toEqual('p____') + game.guess('e') + expect(game.getMaskedWord()).toEqual('pe___') + game.guess('a') + expect(game.getMaskedWord()).toEqual('pea__') + game.guess('c') + expect(game.getMaskedWord()).toEqual('peac_') + game.guess('h') + expect(game.getMaskedWord()).toEqual('peach') + expect(game.getStatus()).toEqual(GAME_STATUS.WIN) + }) + + xtest('Winning on last guess still results in a win', () => { + let game = new Hangman('yyyyy') + let counter = 9 + while (counter > 0) { + game.guess('h') + counter -= 1 + } + expect(game.getMaskedWord()).toEqual('_____') + game.guess('y') + expect(game.getMaskedWord()).toEqual('yyyyy') + expect(game.getStatus()).toEqual(GAME_STATUS.WIN) + }) +}); diff --git a/exercises/hangman/package.json b/exercises/hangman/package.json new file mode 100644 index 0000000000..893bf6fdd0 --- /dev/null +++ b/exercises/hangman/package.json @@ -0,0 +1,35 @@ +{ + "name": "exercism-javascript", + "description": "Exercism exercises in Javascript.", + "author": "Katrina Owen", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript" + }, + "devDependencies": { + "@babel/cli": "^7.5.5", + "@babel/core": "^7.5.5", + "@babel/preset-env": "^7.5.5", + "@types/jest": "^24.0.16", + "@types/node": "^12.6.8", + "babel-eslint": "^10.0.2", + "babel-jest": "^24.8.0", + "eslint": "^6.1.0", + "eslint-plugin-import": "^2.18.2", + "jest": "^24.8.0" + }, + "jest": { + "modulePathIgnorePatterns": [ + "package.json" + ] + }, + "scripts": { + "test": "jest --no-cache ./*", + "watch": "jest --no-cache --watch ./*", + "lint": "eslint .", + "lint-test": "eslint . && jest --no-cache ./* " + }, + "license": "MIT", + "dependencies": {} +}