4
\$\begingroup\$

I recently posted my Hangman game, and got some amazing feedback! The main thing suggested was to create a word generator that could read JSON files so that my hangman brain class didn't have to generate the word used on top of handling gameplay. To do this, I created the HangmanWordGenerator class that parses JSON and generates a word for the HangmanBrain class. Before, the HangmanBrain used a set "database" in the form of three arrays (one for each difficulty) and picked a word itself. I am wondering how I did in implementing these improvements.

Hangman Brain

import Foundation
import Darwin

//MARK: - Global Enum

enum DifficultyLevel: String {
case Easy = "Easy"
case Medium = "Medium"
case Hard = "Hard"
}

class HangmanBrain {

init() {
    //not sure what to do here? without an empty init xcode says I have to use the one below
}

init(word: String) {
    self.word = word //when the user inputs a word
}

//MARK: - Properties

var level: DifficultyLevel? {
    didSet {
        wordGenerator = HangmanWordGenerator(level: level!)
        word = wordGenerator!.generateWord()
        setNumberOfGuesses(level: level!)
    }
}

var wordGenerator : HangmanWordGenerator? = nil

var guesses: Int? = nil

var word: String? = nil {
    didSet {
        makeNewGameWord()
    }
}

var gameWord = ""

//MARK: - Gameplay Functions

private func makeNewGameWord() {
    gameWord = ""
    for _ in word!.characters {
        createGameWord(character: "_")
    }
}

private func setNumberOfGuesses(level level: DifficultyLevel) {
    switch level.rawValue {
    case "Easy": guesses = 10
    case "Medium": guesses = 8
    case "Hard": guesses = 6
    default: break
    }
}

func checkGuessAndUpdateGameWordAndGuesses(character character: String) {
    var guessIsCorrect = false
    let answer = word!
    let currentWord = gameWord as String
    gameWord = ""
    let currentWordTrimmed = currentWord.stringByReplacingOccurrencesOfString(" ", withString: "")
    let numberOfLetters = answer.characters.count as Int
    for i in 0...numberOfLetters-1 {
        let start = advance(currentWordTrimmed.startIndex, i)
        let end = advance(currentWordTrimmed.startIndex, i+1)
        let subCurrentWord = currentWordTrimmed.substringWithRange(Range<String.Index>(start: start, end: end))
        if subCurrentWord != "_" {
            createGameWord(character: subCurrentWord)
        } else {
            let subAnswer = answer.substringWithRange(Range<String.Index>(start: start, end: end))
            if subAnswer == character.lowercaseString {
                guessIsCorrect = true
                createGameWord(character: subAnswer)
            } else {
                createGameWord(character: "_")
            }
        }
    }
    if(!guessIsCorrect) {
        guesses = guesses! - 1
    }
}

func buildCorrectWord() {
    gameWord = ""
    for c in word!.characters {
        createGameWord(character: "\(c)")
    }
}

func createGameWord(character character: String) {
    gameWord += "\(character) "
}

func theUserWon() -> Bool {
    for ch in gameWord.characters {
        if "\(ch)" == "_" {
            return false
        }
    }
    return true
}

func theUserLost() -> Bool {
    return guesses == 0
}
}

Hangman Word Generator

import Foundation

class HangmanWordGenerator {

//MARK: - Properties

var wordDatabase: NSArray? = nil {
    didSet {
        print(wordDatabase)
    }
}

var level: DifficultyLevel?

//MARK: - Init functions

init(level: DifficultyLevel) {
    self.level = level
    switch level.rawValue {
    case "Easy": wordDatabase = parseJSONForArrayOfWords(filepath: "EasyWords")
    case "Medium": wordDatabase = parseJSONForArrayOfWords(filepath: "MediumWords")
    case "Hard": wordDatabase = parseJSONForArrayOfWords(filepath: "hardWords")
    default: break
    }
}

init(filepath: String) {
    wordDatabase = parseJSONForArrayOfWords(filepath: filepath)
}

//MARK: - Parse and Generate Functions

func parseJSONForArrayOfWords(filepath filepath: String) -> NSArray? {
    let bundle = NSBundle.mainBundle()
    let path = bundle.pathForResource(filepath, ofType: "json")!
    let jsonData: NSData = NSData(contentsOfFile: path)!
    do {
        let JSONDict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions(rawValue: 0)) as? NSDictionary
        if let dictionary = JSONDict {
            if let wordList = dictionary["wordList"] as? NSDictionary {
                if let database = wordList["list"] as? NSArray {
                    return database
                }
            }
        }
    } catch let error as NSError {
        print("\(error)")
    }
    return nil
}

func generateWord() -> String {
    let dataBase = wordDatabase!
        let UInt = UInt32(dataBase.count - 1)
        let wordNumber = Int(arc4random_uniform(UInt))
        return dataBase[wordNumber] as! String
}
}
\$\endgroup\$
6
\$\begingroup\$

This is a better approach than your previous question, but let's make it even better, shall we? Let's take a protocol-oriented approach to this problem.

What methods does the HangmanWordGenerator implement that the HangmanBrain actually need? Realistically, only generateWord, right?

So let's make a protocol and have the HangmanBrain have it's generator's type be this protocol, shall we?

protocol HangmanWordGenerator {
    func nextWord() -> String?
}

Now, in our HangmanBrain, this is our type:

var wordGenerator: HangmanWordGenerator?

So now, we can just make classes or structs that implement this protocol to use as our generator. So perhaps we rename your current generator class as something marking at it as a JSON word generator?

class JSONWordGenerator : HangmanWordGenerator { // etc...

Now later, I could extend your game by creating my own class that perhaps grabs words from some online dictionary...

class OnlineDictionaryWordGenerator : HangmanWordGenerator { // etc...

All your HangmanBrain class needs is a means for getting a word to play (by way of nextWord() that the protocol requires). By using a protocol rather than a class, you leave 100% of the implementation details up to whomever is extending the game with this new means of generating a word.

They don't have to inherit any of the code you've written.


init() {
    //not sure what to do here? without an empty init xcode says I have to use the one below
}

init(word: String) {
    self.word = word //when the user inputs a word
}

These constructors don't really make sense.

When we're constructing a brain, we don't immediately need a word. What we need is a generator. We'll only ask our generator for a word whenever it's time to start a game.

It also probably makes sense to grab a difficulty level during the constructor. So we want something like this:

init(generator: HangmanWordGenerator, difficulty: DifficultyLevel)

switch level.rawValue {
case "Easy": guesses = 10
case "Medium": guesses = 8
case "Hard": guesses = 6
default: break
}

It's not even entirely necessary for our enum to be String backed. But the place to use the backing data isn't in our switches. This is entirely inefficient and make matching more difficult. Instead, just switch on the value without expanding it to its raw value.

switch level {
case .Easy: guesses = 10
case .Medium: guesses = 8
case .Hard: guesses = 6
}

Doing it this way also means we're not required to have a default case.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.