[iOS-Swift] 심화 문법 문제 - 숫자 야구 게임

TIL 18일 차 - Swift로 야구 게임 만들기- Trouble shooting 

⚾️ 야구게임 과제 깃허브 링크 🔗

 

1. 사용자 입력받기 (게임 선택)

(1) guard let으로 readLine() 받기

// 맨 처음 작성한 사용자 입력 로직 (수정 전)

while true { // 번호 선택 로직 반복
	// 게임 선택 번호 출력문
    guard let inputNum = readLine(), // 정상적으로 값이 입력 되었는지, nil이면 바로 else로
    	  let inputNumber = Int(inputNum) // 문자열을 숫자로 변환가능 여부, 숫자가 아니면 nil -> else로
          // 입력값 검사 함수 호출해서 참이면 true값 return
} else {
	continue // 입력값이 nil이거나, 숫자로 변환 실패하면 다시 입력 받음
}

 

 

(2) switch casereadLine() 받기

// 최종 리팩토링한 사용자 입력값 받는 로직 (수정 후)

while true {
    switch readLine() {
    case "1": // 어차피 readLine은 정수1을 입력해도 문자열 "1"로 받음
    	// 야구게임 시작 메서드 실행
    case "2":
    	// 야구게임 기록 메서드 실행
    case "3":
    	exit(0) // 강제 종료 함수 실행
    default:
    	// "1", "2", "3" 제외한 나머지 nil값 포함한 입력값일때
        //다시 while문 돌기
    }
}

 

 

 

2. 사용자 입력 값 검증 (게임용 3자리 수)

튜터님의 소중한 리뷰

진짜 몰랐다.. flatMap은 고작 2차원 배열을 1차원 배열로 껍데기만 벗기는 애.인 줄 알았는데 optional 벗기는 역할도 하는구나...!!!

공부 더 열심히 하자..

guard let inputNumber = readLine().flatMap({ Int($0) }, gameCenter.checkInput(inputNumber) else {
  ...
}

 

근데 왜 Map도 아니고 flatMap이냐!!?

만약 map으로 쓰게 되면..

readLine().map { Int($0) }

String? -> Int?? 옵셔널이 두겹이 됨

 

이러한 문제가 생겨 여기서 flatMap이 등장하게 된다.

flatMap은 옵셔널 안에서 또 옵셔널이 나오면 한 겹으로 flat 하게 만들어주게 한다.

그래서 결과 타입은 Int? 값이 나오게 된다.

 

<최종 case별 경우>

 

1. 입력이 없는 경우

-> nil -> flatMap 실행 안 함 -> 결과: nil

 

2. 입력이 "123"

->Int("123") = Optional(123)

-> flatMap -> Optional(123)

 

3. 입력이 "abc"

-> Int("abc") = nil

-> flatMap -> nil

 

 

3. 사용자 입력값 중복 검증 메서드

(1) 배열의 인덱스를 이용해 단순 중복 비교

// 맨 처음 작성한 사용자 입력값과 정답 비교 로직 (수정 전)

let arr = splitNum(inputNumber)
        
        if arr[0] != arr[1]
            && arr[1] != arr[2]
            && arr[0] != arr[2]
// 이후 생략

마스터반의 인간 지피티 최다 리뷰어 빛예린님

사진에 있는 리뷰처럼 중복을 배열로 검증하는 것도 괜찮지만 Array를 Set으로 변환하여 count를 이용해 개수가 3일 때!라고 한 줄로 깔끔하게 정리할 수 있다.

 

(2) Array를 Set으로 변환 후 count 이용해 검증

// Set으로 변환후 count로 중복 체크 (수정 후)

func checkInput(_ inputNumber: Int) -> Bool {
    let set = Set(splitNum(inputNumber))
        
    if set.count == 3   // Array를 Set으로 변환하여 중복을 제외한 값이 3이어야 함
        && 99 < inputNumber
        && inputNumber < 1000 {
        return true
    } else {
        return false
    }
}

 

 

 

4. 정답 만드는 메서드

(1) Int.random(in: ) 이용해 단순 반복 생성하기

// 맨 처음 작성한 정답 만드는 메서드 (수정 전)
// 1에서 9까지의 서로 다른 임의의 정답인 수 3개를 정하기 (abc)
    func makeAnswer() -> Array<Int> {
                
        let a = Int.random(in: (1...9))
        
        var b = Int.random(in: 0...9)
        while a == b {
            b = Int.random(in: 0...9) // a와 b가 다를때까지 b에 랜덤한 Int값 대입
        }
        
        var c = Int.random(in: 0...9)
        while a == c || b == c {
            c = Int.random(in: 0...9) // c가 a, b값과 다를때까지 c에 랜덤한 Int값 대입
        }
        
        let answer: Array = [a, b, c]
        return answer
    }

 

 

(2) Set을 이용해 중복 없이 정답 생성하기

// Set을 이용해 정답을 생성하는 메서드 만들기 (수정 후)

    func makeAnswer() -> Array<Int> { // Int 배열을 반환하는 함수 정의
        var answerSet: Set<Int> = []
        var answerArray: [Int] = [] // 빈 배열과 Set 생성하기
        
        let first = Int.random(in: 1...9) // 백의 자리 수는 1부터 9까지
        answerSet.insert(first)
        answerArray.append(first)// 백의 자리 수 배열에 넣기 (먼저 안넣으면 백의자리에 0 가능해짐)
        
        while answerSet.count < 3 { // Set을 이용해 count가 3이 될때까지 중복없이 정답 생성하도록 반복
            answerSet.insert(Int.random(in: 0...9))
        }
        
        answerSet.remove(first) // 이미 배열에 first를 넣어두었기 때문에 Set의 백의자리 삭제
        answerArray.append(contentsOf: Array(answerSet)) // Set을 Array로 변환하고 집어넣기
        return answerArray
    }

[문제 상황] 정답의 숫자를 Set을 이용해 만들면 순서가 보장되지 않아 백의 자리가 0이 되는 경우가 발생한다.

 

[해결] 결과는 Array로 저장할 것이므로 first 상수를 배열에 먼저 저장해 두기

근데 이 Set으로 만든 방식은 너무 복잡하고 코드도 길어서 별로다.

 

 

(3) 배열의 contains를 이용해 생성하기 (리뷰대로 해보기)

    // 배열의 contains를 이용해 생성하기 (수정 후)
    
    func makeAnswer() -> Array<Int> {
        var answerArray: Array<Int> = []
        answerArray.append(Int.random(in: 1...9)) // 백의 자리 수는 1부터 9까지

        while answerArray.contains(answerArray[0]) {
            answerArray.append(Int.random(in: 0...9))
        }

        while answerArray.contains(answerArray[0]) || answerArray.contains(answerArray[1]) {
            answerArray.append(Int.random(in: 0...9))
        }

        return answerArray
    }

 

5. 사용자 입력값, 정답 비교 메서드

 

(1) 배열의 enumerated() 함수를 이용해 for, if문으로 인덱스와 값 단순 비교

// 맨 처음 작성한 사용자 입력값과 정답 비교 로직 (수정 전)

let inputArray = splitNum(number) // 사용자가 입력한 값을 각각 한자리수의 원소를 가진 배열로 변환
        
for (ansIdx, ansEle) in ansArray.enumerated(){ // enumerated 함수이용해 정답 배열을 튜플로 나누기
        for (iptIdx, iptEle) in inputArray.enumerated() { // 같은 방식으로 사용자 입력값도 튜플로 나누기
            if ansEle == iptEle { // 두 값이 같을때
                if ansIdx == iptIdx { // 인덱스 값도 같을때
                    strike += 1
                } else {
                    ball += 1
                }
            }

 

(2) 배열의. contains() 함수를 이용해 더 간결하게 리팩토링

// 배열의 .contains() 함수를 이용해 리팩토링 (수정 후)

for i in 0..<3 {
            if inputArray[i] == ansArray[i] { // 순서대로 비교한 값이 같으면 스트라이크
                strike += 1
            } else if ansArray.contains(inputArray[i]) {
            // 그렇지 않을때 정답 배열의 다른 인덱스 위치에 해당하는 값을 가지고 있으면 볼
                ball += 1
            }

 

 

7. 문자열로 게임 결과를 판단하던 구조 개선

[문제 상황]

// 문자열로 결과값 return (수정 전)
return "collect"
return "False"

- 문자열 비교로 분기 처리, 오타 발생 시 런타임 오류 가능

 

[해결]

- 문자열 대신 enum으로 결과 표현하기

enum GameResult {
    case correct
    case nothing
    case progress(strike: Int, ball: Int)
}

의미 있는 값은 타입(enum)으로 표현하는 것이 안전하다.

// 문자열 대신 enum값으로 return (수정 후)
if (strike == 3) {
        return GameResult.correct
    } else if (strike == 0 && ball == 0){
        return GameResult.nothing
    } else {
        return GameResult.progress(strike: strike, ball: ball)
    }