7
\$\begingroup\$

I am a Java programmer trying to learn the ways of Swift. I coded a Hangman game in Xcode. I was wondering what I could improve, specifically whether I used delegation correctly and if there is anything I can do more elegantly in Swift.

Title View Controller

import UIKit

@IBDesignable
class TitleViewController: UIViewController {

//Mark - Properties

@IBOutlet weak var hangmanTitleLabel: UILabel! {
    didSet {
        let hangmanTitle: NSString = "Hangman"
        let attributes = [NSFontAttributeName: UIFont(name: "MarkerFelt-Thin", size: 48.0)!, NSForegroundColorAttributeName: UIColor.redColor()]
        let titleString = NSAttributedString(string: hangmanTitle as String, attributes: attributes)
        hangmanTitleLabel.attributedText = titleString
    }
}

//MARK - Lifecycle functions

override func shouldAutorotate() -> Bool {
    return false
}

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.Portrait
}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let destination = segue.destinationViewController as? GameViewController {
        if let identifier = segue.identifier {
            switch identifier {
            case "Easy": destination.brain.level = "Easy"
            case "Medium": destination.brain.level = "Medium"
            case "Hard": destination.brain.level = "Hard"
            default: break
            }
        }
    }
}
}

TitleView

import UIKit

//MARK - Global Functions

func connectPoints(bottomLeftPoint: CGPoint, bottomRightPoint: CGPoint, topLeftPoint: CGPoint, topRightPoint: CGPoint, color: UIColor) {
color.set()

let path = UIBezierPath()
path.moveToPoint(bottomLeftPoint)
path.addLineToPoint(topLeftPoint)
path.addLineToPoint(topRightPoint)
path.addLineToPoint(bottomRightPoint)
path.closePath()
path.fill()
path.stroke()
}

func calculateMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2)
}

class TitleView: UIView {

//MARK - Drawing Scales and Constants

struct DrawingConstants {
    static let gallowBaseStartScale: CGFloat = 0.15
    static let gallowBaseEndScale: CGFloat = 0.85
    static let gallowBaseHeight: CGFloat = 10
    static let gallowHeight: CGFloat = 0.15
    static let gallowHeightStart: CGFloat = 0.175
    static let gallowHeightWidth: CGFloat = 10
    static let gallowAcrossScale: CGFloat = 0.5
    static let gallowTipHeight: CGFloat = 17.5
    static let headRadius: CGFloat = 16
    static let bodyLength: CGFloat = 25
    static let bodyHeight: CGFloat = 25
    static let legLength: CGFloat = 50
    static let grassHeightScale: CGFloat = 0.68
    static let armBack: CGFloat = 5
}

//MARK - Drawing Functions

override func drawRect(rect: CGRect) {
    drawGrass()
    drawSky()
    drawGallow()
    drawDude()
}

func drawGrass() {
    let topStartPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
    let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: topStartPoint.y)
    let bottomRightPoint = CGPoint(x: topRightPoint.x, y: CGFloat(bounds.size.height))
    let bottomLeftPoint = CGPoint(x: CGFloat(0), y: bottomRightPoint.y)

    connectPoints(bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topStartPoint, topRightPoint: topRightPoint, color: UIColor.greenColor())
}

func drawSky() {
    let bottomLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
    let topLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(0))
    let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(0))
    let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))

    connectPoints(bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.cyanColor())
}

func drawGallow() {
    drawGallowBase()
    drawGallowHeight()
    drawGallowAcross()
    drawGallowTip()
}

func drawGallowBase() {
    let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseStartScale), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
    let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bottomLeftPoint.y - DrawingConstants.gallowBaseHeight)
    let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseEndScale), y: topLeftPoint.y)
    let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)

    connectPoints(bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brownColor())
}

func drawGallowHeight() {
    let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale - DrawingConstants.gallowBaseHeight))
    let bottomRightPoint = CGPoint(x: bottomLeftPoint.x + DrawingConstants.gallowHeightWidth, y: bottomLeftPoint.y)
    let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bounds.size.height * DrawingConstants.gallowHeight)
    let topRightPoint = CGPoint(x: bottomRightPoint.x, y: topLeftPoint.y)

    connectPoints(bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brownColor())
}

func drawGallowAcross() {
    let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart) + DrawingConstants.gallowHeightWidth, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
    let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: bottomLeftPoint.y)
    let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight))
    let topRightPoint = CGPoint(x: CGFloat(bottomRightPoint.x), y: topLeftPoint.y)
    connectPoints(bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brownColor())
}

func drawGallowTip() {
    let topLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - DrawingConstants.gallowHeightWidth), y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
    let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: topLeftPoint.y)
    let bottomLeftPoint = CGPoint(x: topLeftPoint.x, y: topLeftPoint.y + DrawingConstants.gallowTipHeight)
    let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)

    connectPoints(bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brownColor())
}

private func drawDude() {
    drawHead()
    drawBody()
}

func drawHead() {
    let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
    let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + DrawingConstants.headRadius)
    let center = CGPoint(x: centerX, y: centerY)

    UIColor.blackColor().set()
    let path = UIBezierPath(arcCenter: center, radius: DrawingConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
    path.lineWidth = CGFloat(2)
    path.stroke()
}

private func drawBody() {
    let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * DrawingConstants.headRadius)
    let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
    let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
    let startPoint = CGPoint(x: startPointX, y: startPointY)
    let midPoint = CGPoint(x: startPoint.x + DrawingConstants.bodyLength, y: startPoint.y + DrawingConstants.bodyHeight)
    let endPoint = CGPoint(x: midPoint.x + DrawingConstants.legLength, y: midPoint.y)
    let bodyMid = calculateMidPoint(startPoint, point2: midPoint)
    let armStartX = CGFloat(bodyMid.x - DrawingConstants.armBack)
    let armStartY = CGFloat(bodyMid.y - DrawingConstants.armBack)
    let armStart = CGPoint(x: armStartX, y: armStartY)
    let armMid = CGPoint(x: armStart.x, y: midPoint.y)
    let armEnd = CGPoint(x: bodyMid.x + DrawingConstants.armBack, y: armMid.y)
    let legStart = calculateMidPoint(midPoint, point2: endPoint)
    let legEndX = calculateMidPoint(legStart, point2: endPoint).x
    let legEndY = endPoint.y
    let legMidX = legStart.x
    let legMidY = armStartY
    let legMid = CGPoint(x: legMidX, y: legMidY)
    let legEnd = CGPoint(x: legEndX, y: legEndY)

    UIColor.blackColor().set()
    let path = UIBezierPath()
    path.lineWidth = CGFloat(2)
    path.moveToPoint(startPoint)
    path.addLineToPoint(midPoint)
    path.addLineToPoint(endPoint)
    path.stroke()
    path.moveToPoint(armStart)
    path.addLineToPoint(armMid)
    path.addLineToPoint(armEnd)
    path.moveToPoint(midPoint)
    path.addLineToPoint(legMid)
    path.addLineToPoint(legEnd)
    path.stroke()
}
}

GameView Controller

import UIKit

class GameViewController: UIViewController, gameViewDataSource {

//MARK - Properties

@IBOutlet weak var gameView: GameView! {
    didSet {
        gameView.dataSource = self
    }
}

@IBOutlet weak var youLose: UILabel! {
    didSet {
        youLose.textColor = UIColor.cyanColor()
        youLose.font = UIFont(name: "MarkerFelt-Thin", size: CGFloat(48.0))
    }
}


@IBOutlet weak var youWin: UILabel! {
    didSet {
        youWin.textColor = UIColor.cyanColor()
        youWin.font = UIFont(name: "MarkerFelt-Thin", size: CGFloat(48.0))
    }
}


@IBOutlet weak var numberOfGuessesLabel: UILabel! {
    didSet {
        if let guesses = brain.guesses {
            numberOfGuessesLabel.text = "\(guesses)"
        }
    }
}

@IBOutlet weak var gameWordLabel: UILabel! {
    didSet {
        let gameWord = brain.gameWord
        gameWordLabel.text = gameWord
    }
}

@IBAction func guess(sender: UIButton) {
    if running {
        let guessedAlready = sender.currentTitleColor
        if guessedAlready == UIColor.redColor() {
            return
        } else {
            sender.setTitleColor(UIColor.redColor(), forState: .Normal)
            let guess = sender.currentTitle!
            brain.checkGuessAndUpdateGameWordAndGuesses(character: guess)
            numberOfGuessesLabel.text = "\(brain.guesses!)"
            gameWordLabel.text = brain.gameWord
            gameView.setNeedsDisplay()
            checkYouWin()
            checkYouLose()
        }
    }
}

var running = true

var brain = HangmanBrain()

//MARK - Lifecycle Functions

override func shouldAutorotate() -> Bool {
    return false
}

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.Portrait
}

//MARK - Gameplay Methods

func numberOfGuessesLeft() -> Int {
    return brain.guesses!
}

func gameLevel() -> String {
    return brain.level
}

func checkYouLose() {
    if brain.theUserLost() {
        running = false
        youLose.textColor = UIColor.redColor()
        brain.buildCorrectWord()
        gameWordLabel.text = brain.gameWord
    }
}

func checkYouWin() {
    if brain.theUserWon() {
        running = false
        youWin.textColor = UIColor.redColor()
    }
}
}

GameView

import UIKit

//MARK - GameView protocol

protocol gameViewDataSource: class {
func numberOfGuessesLeft() -> Int
func gameLevel() -> String
}

//MARK - Global Function

func drawLine(startPoint: CGPoint, endPoint: CGPoint) {
let path = UIBezierPath()
path.lineWidth = CGFloat(2)
path.moveToPoint(startPoint)
path.addLineToPoint(endPoint)
path.stroke()
}

class GameView: TitleView {

//MARK - Drawing Scales and Constants

struct ScaleConstants {
    static let bodyLength: CGFloat = 50
    static let limbLength: CGFloat = 25
    static let handHeightScale: CGFloat = 0.4
    static let headRadius: CGFloat = 20
    static let eyeRadius = CGFloat(0.15 * ScaleConstants.headRadius)
    static let eyeOffset = CGFloat(0.3 * ScaleConstants.headRadius)
    static let mouthOffSet = CGFloat(0.3 * ScaleConstants.headRadius)
    static let mouthRadius = CGFloat(0.25 * ScaleConstants.headRadius)
}

//MARK - Properties

weak var dataSource = gameViewDataSource?()

private var bodyStart: CGPoint = CGPointZero

private var bodyEnd: CGPoint = CGPointZero

private var headMiddle: CGPoint = CGPointZero

//MARK - Drawing functions

override func drawRect(rect: CGRect) {
    drawSky()
    drawGrass()
    drawGallow()
    let level = dataSource?.gameLevel()
    let guesses = dataSource?.numberOfGuessesLeft()
    var wrongGuessesSoFar = 0
    var maxGeusses = 0
    switch level! {
        case "Hard":
            maxGeusses = 6
        case "Medium":
            maxGeusses = 8
        case "Easy":
            maxGeusses = 10
    default: break
    }
    wrongGuessesSoFar = maxGeusses - guesses!
    startDrawChain(wrongGuessesSoFar)
}

func startDrawChain(numberOfGuesses: Int) {
    drawHead(numberOfGuesses)
}

func drawHead(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
    let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
    let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + ScaleConstants.headRadius)
    let center = CGPoint(x: centerX, y: centerY)
    headMiddle = center

    UIColor.blackColor().set()
    let path = UIBezierPath(arcCenter: center, radius: ScaleConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
    path.lineWidth = CGFloat(2)
    path.stroke()
    drawBody(numberOfGuesses - 1)
    }
}

func drawBody(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
    UIColor.blackColor().set()
    let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * ScaleConstants.headRadius)
    let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
    let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
    let startPoint = CGPoint(x: startPointX, y: startPointY)
    let endPoint = CGPoint(x: startPoint.x, y: startPoint.y + ScaleConstants.bodyLength)
    bodyStart = startPoint
    bodyEnd = endPoint
    drawLine(startPoint, endPoint: endPoint)
    drawLeftLeg(numberOfGuesses - 1)
    }
}

func drawLeftLeg(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
        let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
        let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
        drawLine(startPoint, endPoint: endPoint)
        drawRightLeg(numberOfGuesses - 1)
    }
}

func drawRightLeg(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
        let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
        let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
        drawLine(startPoint, endPoint: endPoint)
        drawLeftArm(numberOfGuesses - 1)
    }
}

func drawLeftArm(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
        let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
        let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
        drawLine(startPoint, endPoint: endPoint)
        drawRightArm(numberOfGuesses - 1)
    }
}

func drawRightArm(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
        let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
        let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
        drawLine(startPoint, endPoint: endPoint)
        drawLeftEye(numberOfGuesses - 1)
    }
}

func drawLeftEye(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
        UIColor.blackColor().set()
        let eyeMiddle = CGPoint(x: headMiddle.x - ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)

        let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
        path.lineWidth = CGFloat(1)
        path.stroke()
        drawRightEye(numberOfGuesses - 1)
    }
}

func drawRightEye(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
        UIColor.blackColor().set()
        let eyeMiddle = CGPoint(x: headMiddle.x + ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)

        let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
        path.lineWidth = CGFloat(1)
        path.stroke()
        drawMouth(numberOfGuesses - 1)
    }
}

func drawMouth(numberOfGuesses: Int) {
    if numberOfGuesses == 0 {
        return
    } else {
        UIColor.blackColor().set()
        let mouthMiddle = CGPoint(x: headMiddle.x, y: headMiddle.y + ScaleConstants.mouthOffSet)

        let path = UIBezierPath(arcCenter: mouthMiddle, radius: ScaleConstants.mouthRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
        path.lineWidth = CGFloat(1)
        path.stroke()
    }
}
}

Hangman Brain

import Foundation
import Darwin

class HangmanBrain {

//MARK - Database of Words

private struct Words {
    static let Easy: [String] = ["fireplace","apple","january","tooth","cookies","mysterious","essential","magenta","darling","pterodactyl"]
    static let Medium: [String] = ["palace","thumb","eleven","monkey","hunter","wounds","wright","egypt","slaves","zipper"]
    static let Hard: [String] = ["jazz","puff","jiff","sphinx","vex","pox","hajj","jinx","vine","mom"]
    static let numberOfWordsPerLevel = 10
}

//MARK - Properties

var level: String = "" {
    didSet {
        chooseWord(wordLevel: level)
        setNumberOfGuesses(level: level)
    }
}
var guesses: Int? = nil

var word: String? = nil

var gameWord = ""

private let wordsByLevel : [String: [String]] = ["Easy": Words.Easy, "Medium": Words.Medium, "Hard": Words.Hard]

//MARK - Gameplay Functions

private func chooseWord(wordLevel wordLevel: String) {
    let UInt = UInt32(Words.numberOfWordsPerLevel - 1)
    let wordNumber = Int(arc4random_uniform(UInt))
    let wordChosen = wordsByLevel[wordLevel]![wordNumber]
    word = wordChosen
    gameWord = ""
    for _ in wordChosen.characters {
        createGameWord(character: "_")
    }
}

private func setNumberOfGuesses(level level: String) {
    switch level {
    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
}
}
\$\endgroup\$
7
\$\begingroup\$

There's a ton of code here. For the purposes of this review, I'll be focusing on the HangmanBrain class (I've ignored everything else for this answer). This class seems to be the core, so it's a good place to start.


private struct Words {
    static let Easy: [String] = ["fireplace","apple","january","tooth","cookies","mysterious","essential","magenta","darling","pterodactyl"]
    static let Medium: [String] = ["palace","thumb","eleven","monkey","hunter","wounds","wright","egypt","slaves","zipper"]
    static let Hard: [String] = ["jazz","puff","jiff","sphinx","vex","pox","hajj","jinx","vine","mom"]
    static let numberOfWordsPerLevel = 10
}

I am a fan of declaring constants within structs like this. However, I don't like what's being done here.

We've severely limited the expandability of our game.

Instead of this struct, our HangmanBrain class should be reworked to read in JSON to construct its word list. For example:

EasyWords.json

{
    "wordList" : {
        "difficulty" : "easy",
        "list" : [
            "fireplace",
            "apple",
            "january",
            "tooth",
            "cookies",
            "mysterious",
            "essential",
            "magenta",
            "darling",
            "pterodactyl"
        ]
    }
}  

Importantly here, we've defined a JSON structure. Our app can come preloaded with EasyWords.json, MediumWords.json, and HardWords.json, but we can then easily add word lists in future updates just by adding JSON files. As well, we can allow our app to download word lists from online. We either simply point our app toward some API endpoint we develop, or we can come up with a way for the user to point toward an endpoint of their choosing to download custom word lists. Of course, none of this is necessary yet... but if we ever want it to be an available option, we need to start with a more flexible way of loading our word lists in the first place.


In addition to stripping the hard-coded word list from our source code and adding it as a resource, we should rethink how we're getting words.

The HangmanBrain class has too much responsibility. It should only be in charge of playing the game. It shouldn't be in charge of generating the word to be played. It should be told what word wants to be played. So, let's use a property of type Generator<String> and just call next() on it to get the word to play.

Now we can give our HangmanBrain game a generator to use. One generator might load our objects from a JSON file and serve them up in random order. Another might load the strings from a web server and serve them up in a predetermined order. We can think of a million different ways to generate lists of words for the game to use. Maybe one generator is based off user-inputted words for a multiplayer hangman? When we decouple the list from the brain, we allow a lot more flexibility in our code.

So, for example...

class HangmanBrain {
    // set this in init
    var wordGenerator: Generator<String>

    /* stuff */

    // now, any time we need a word, we just call:
    self.wordGenerator.next()
    // when this call returns nil, the generator is out of words
    // but generators don't have to ever return nil
}

Lastly (for this review), I think it's very important that we not use strings for setting the difficulty level.

Instead, let's provide a difficulty enum:

class HangmanBrain {
    enum Difficulty: String {
        case Easy = "easy"
        case Medium = "medium"
        case Hard = "hard"
    }
    // other stuff
}

The enum is still backed by a string, so when we need that string value for parsing JSON, we can access it (HangmanBrain.Difficulty.Easy.rawValue), but importantly, we've made it very explicit what sort of values to accept when we're looking for a difficulty.

\$\endgroup\$
  • \$\begingroup\$ That's all I have for now. I think it's crucially important that you really, really, really think about these ideas and concepts. You should implement them and resubmit just the HangmanBrain class (plus perhaps a HangmanWordGenerator class). I'd either omit the piles of UI code you dropped in here or post them in their own question if you're especially concerned about a review of those aspects. But with all that said, I think it's crucial to get the backend stuff done right first. Getting that done right makes everything else fall into place. \$\endgroup\$ – nhgrif Jul 20 '15 at 23:51
  • \$\begingroup\$ Thank you very much for the review! I will redo the HangmanBrain class and create second generator class. The only thing I am unsure of is how I would make a generator that reads a JSON file? \$\endgroup\$ – Jared Jul 21 '15 at 1:01
  • \$\begingroup\$ Also sorry to dump the UI code. Looking back I'm really not sure why I did that \$\endgroup\$ – Jared Jul 21 '15 at 1:08
  • \$\begingroup\$ @Jared Do some research on SO regarding Swift's generator protocol and reading/parsing JSON. Two separate problems to solve. Treat them as separate problems. \$\endgroup\$ – nhgrif Jul 21 '15 at 1:22

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.