Roen의 iOS 개발로그

다트 게임 - Swift; 객체를 이용한 풀이

by Steady On

[프로그래머스 - 다트 게임] - 난이도 Lv. 1

https://school.programmers.co.kr/learn/courses/30/lessons/17682

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

문제 분석

구해야 하는 것

0~10의 정수와 문자 S, D, T, *, #로 구성된 문자열이 입력될 시 총점수를 반환하는 함수 → return type은 Int

 

문제에서 주어진 것

  1. 입력 형식: "점수|보너스|[옵션]"으로 이루어진 문자열 3세트. 예) 1S2D*3T
  2. 점수는 0에서 10 사이의 정수이다.
  3. 보너스는 Single(S), Double(D), Triple(T) 중 하나이다.
    • 각 영역 당첨 시 점수에서 1제곱, 2제곱, 3제곱 (점수1 , 점수2 , 점수3)으로 계산된다.
  4. 옵션은 *이나 # 중 하나이며, 없을 수도 있다.
    • 스타상(*) 당첨 시 해당 점수와 바로 전에 얻은 점수를 각 2배로 만든다.
    • 아차상(#) 당첨 시 해당 점수는 마이너스된다.
    • 스타상(*)은 첫 번째 기회에서도 나올 수 있다. 이 경우 첫 번째 스타상(*)의 점수만 2배가 된다.
    • 스타상(*)의 효과는 아차상(#)의 효과와 중첩될 수 있다. 이 경우 중첩된 아차상(#)의 점수는 -2배가 된다.

 

어떻게 풀어볼까?

구현이 필요한 함수

  1. 입력 값을 "점수|보너스|[옵션]"으로 분리하여 각 기회당의 점수, 보너스, 옵션으로 분리하는 함수
  2. 각각의 기회에 부여된 옵션에 따라 직전 점수를 두배로 만드는 함수
  3. 각 기회에서 얻은 점수의 총 합을 리턴하는 함수

객체 구현

1번 함수 구현 시 각 기회의 점수, 보너스, 옵션의 정보를 어떻게 저장하면 좋을까 고민했다. 일단 점수는 정수, 보너스와 옵션은 문자열이라 배열에 담는 것은 안되고, 또 각각의 기회마다 얻은 점수는 별개로 또 저장해야 했어서 "기회"라는 것이 가지고 있는 정보가 많다고 판단해서 하나의 구조체로 구현하는 것으로 설계 했다.

struct Chance {
	let point: Int
    let bonus: String
    let option: String?
}

그런데 잘보면 보너스는 S, D, T 중 하나이고, 옵션도 *이나 # 혹은 none(없음) 중 하나여서 열거형으로 만들면 각 기회별 score를 계산할때 보너스나 옵션에 따라 switch문으로 case 분기를 하기에도 좋을 것으로 판단되었다.

enum Bonus: Character {
    case s = "S"
    case d = "D"
    case t = "T"
}

enum Option: Character {
    case star = "*"
    case hashtag = "#"
}

그래서 입력값으로 객체를 만들 수 있도록 rawValue를 Character로 설정해서 입력받는 값으로 만들었다.

struct Chance {
    let point: Int
    let bonus: Bonus
    var option: Option?
}

열거형을 반영한 Chance 구조체다. option은 없을 수도 있으므로 옵셔널 타입으로 만들고, 초기값을 nil로 설정해주었다.

그리고 여기서 이 Chance에서 얻은 점수에 대한 정보도 있어야 하므로 연산 프로퍼티로 구현했다.

struct Chance {
    let point: Int
    let bonus: Bonus
    var option: Option?
    var isRevision: Bool = false
    
    var score: Int {
        var sum = 0
        
        switch bonus {
        case .s: sum = point
        case .d: sum = point * point
        case .t: sum = point * point * point
        }
        
        if let option = self.option {
            switch option {
            case .star: sum *= 2
            case .hashtag: sum *= (-1)
            }
        }
        
        if isRevision { sum *= 2 }
        
        return sum
    }
}

bonus에 따라 포인트를 제곱하고, option을 언래핑한 뒤, 언래핑 되면 옵션으로 인한 연산이 되도록 만들어주었다. 또, 바로 다음 기회에서 얻은 점수가 옵션으로 star("*")를 가지고 있다면, score를 두배로 만들어줘야 하므로 점수 보정 여부를 Bool타입으로 가진 isRevision 변수를 추가해주었다.

 

함수 구현

문자열을 Chance 배열로 변환하여 반환하는 함수; convertStringToChance

func convertStringToChance(_ dartResult: String) -> [Chance] {
    let points = dartResult.split { $0.isNumber == false }.compactMap { Int($0) }
    let bonusAndOption = dartResult.split { $0.isNumber }
    var chances = [Chance]()

    for (point, str) in zip(points, bonusAndOption) {
        let bonus = Bonus(rawValue: str.first!)!
        let option = Option(rawValue: str.last!)

        chances.append(Chance(point: point, bonus: bonus, option: option))
    }

    return chances
}

dartResult를 매개인자로 받아서 숫자와 숫자가 아닌것으로 분리하고, 분리되어진 두 배열을 zip으로 묶어서 Chance 객체를 만들어서 chances 배열에 추가해서 리턴하도록 했다. 이때, option의 경우 들어온 문자열의 last가 보너스의 문자열이라면 rawValue로 Option 객체를 만들었을 때 nil이 될것이다. 그러면, 굳이 문자열의 길이가 1인지 2인지 구별하지 않고도 Chance 인스턴스를 안정적으로 만들 수가 있다.

 

각각의 기회에 부여된 옵션에 따라 직전 점수를 두배로 만드는 함수; reviceScore

func reviceScore(_ chances: [Chance]) -> [Chance] {
    var revicedChances = chances
    
    for index in 1...2 where chances[index].option == .star {
        revicedChances[index-1].isRevision = true
    }
    
    return revicedChances
}

어차피 chances의 길이가 3이므로 1번째 요소부터 돌면서 옵션이 "*"인 경우 앞 인덱스에 있는 Chance의 isRevision 값을 true로 바꿔주게 하였다.

 

점수의 총합을 반환하는 함수; solution

func solution(_ dartResult:String) -> Int {
    let chances = convertStringToChance(dartResult)
    let revicedChances = reviceScore(chances)

    return revicedChances.reduce(0) { $0 + $1.score }
}

보정된 점수를 가지고 있는 revicedChances를 돌면서 score의 합을 구해 리턴하도록 했다.

 

전체 코드

enum Bonus: Character {
    case s = "S"
    case d = "D"
    case t = "T"
}

enum Option: Character {
    case star = "*"
    case hashtag = "#"
}

struct Chance {
    let point: Int
    let bonus: Bonus
    var option: Option?
    var isRevision: Bool = false
    
    var score: Int {
        var sum = 0
        
        switch bonus {
        case .s: sum = point
        case .d: sum = point * point
        case .t: sum = point * point * point
        }
        
        if let option = self.option {
            switch option {
            case .star: sum *= 2
            case .hashtag: sum *= (-1)
            }
        }
        
        if isRevision { sum *= 2 }
        
        return sum
    }
}

func convertStringToChance(_ dartResult: String) -> [Chance] {
    let points = dartResult.split { $0.isNumber == false }.compactMap { Int($0) }
    let bonusAndOption = dartResult.split { $0.isNumber }
    var chances = [Chance]()

    for (point, str) in zip(points, bonusAndOption) {
        let bonus = Bonus(rawValue: str.first!)!
        let option = Option(rawValue: str.last!)

        chances.append(Chance(point: point, bonus: bonus, option: option))
    }

    return chances
}

func reviceScore(_ chances: [Chance]) -> [Chance] {
    var revicedChances = chances
    
    for index in 1...2 where chances[index].option == .star {
        revicedChances[index-1].isRevision = true
    }
    
    return revicedChances
}

func solution(_ dartResult:String) -> Int {
    let chances = convertStringToChance(dartResult)
    let revicedChances = reviceScore(chances)

    return revicedChances.reduce(0) { $0 + $1.score }
}

 

마무리

사실 구현코테 문제에 이렇게 객체까지 구현해서 푸는것은 좀 과했을지도 모르지만, 최근에 공부했던 객체지향 프로그래밍이나 생활체조를 연습해보는 좋은 기회였던 것 같다.

블로그의 정보

Roen의 iOS 개발로그

Steady On

활동하기