ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 야구게임 로직 만들기
    iOS 앱 개발 부트캠프/TIL 2024. 11. 5. 23:40

    1. Lv1, Lv2 시작하기

    알고리즘 생각하기

    이 단계에서 해야 할 것은 정답을 랜덤으로 만들기, 3자리수 입력, 그리고 입력과 정답과 비교해 힌트를 출력하는 것이다.

    랜덤으로 3자리수를 만드는 것은 난수를 생성하는 Int.random(in: 100...999) 를 사용해 100부터 999까지의 숫자를 만들 것이다.

    여기서 주의해야 할 것은 서로 다른 임의의 숫자여야 하기 때문에 100이나 999, 111, 311 같은 중복되는 숫자가 정답으로 생성되면 정답을 재생성 해야한다.

    또한 3자리 수를 입력 받을때도 마찬가지로 올바른 입력인지 체크해야 한다. readLine()을 이용해 입력을 받을 건데, readLine()은 String 타입을 반환하므로 형변환도 고려해야하고, 무엇보다 숫자가 아닌 값이 들어올 수도 있어서 그 부분도 체크해야 한다.

    생성된 정답과 입력 받은 숫자가 형식에 맞는지 확인하고, 정답과 입력을 비교해서 스트라이크와 볼을 출력하려면 글자들을 한 글자씩 분리할 필요가 있다.

    그래야만 0이 포함 되어 있는지, 중복된 숫자가 있는지, 정답과 입력의 위치와 숫자가 같은지 다른지 확인 할 수 있기 때문이다.

    따라서 해야하는 작업이

    1. 정답을 생성해서 한글자씩 분리한 뒤에 형식에 맞는 정답인지 확인해 재설정 여부 판단하기
    2. 입력을 받은 뒤 마찬가지로 한글자씩 분리해서 형식에 맞는 입력인지 확인해 재입력 여부 판단하기
    3. 정답과 입력 모두 형식에 맞다면 둘의 위치와 숫자 비교하기

    이렇게 된다.


    정답 구현하기

    일단 정답을 랜덤으로 생성하는 것은 나중에 해도 늦지 않으니 임의로 정답을 설정하고 이것을 맞출 수 있는지 확인하기로 했다.

    import Foundation
    
    let answer = 628 // 정답
    var answerArray: [String] = [] // 문자로 변환해 하나씩 저장할 예정
    
    for char in String(answer) { // 문자열로 변환해 한글자씩 배열에 저장
        answerArray.append(String(char))
    }

    answerArray는 정답을 한 글자씩 저장할 배열이고, for문을 사용해 String으로 형변환 한 정답을 한 글자씩 answerArray에 입력하게 만들었다.


    입력 받기

    이후 radLine()으로 입력을 받고 while문을 사용해 입력이 정답과 같을때까지, 즉 정답을 맞출때까지 반복하기 위해 while문을 사용하였다.

    print("< 게임을 시작합니다 >")
    print("숫자를 입력하세요")
    var inputNumber = readLine()!
    
    while Int(inputNumber) != answer { // 입력이 정답이랑 같은 값일때까지 반복
        var strike = 0
        var ball = 0
        var inputArray: [String] = [] // 입력을 한글자씩 배열에 저장할 예정
        
        if inputNumber.count == 3 && Int(inputNumber) != nil { // 입력이 3개이고 숫자인지 확인
            for char in inputNumber { // 입력한 숫자 한글자씩 배열에 저장
                inputArray.append(String(char))
            }
            // 정답 배열과 입력 배열이 같은지 확인
            for i in 0..<answerArray.count {
                if inputArray[i] == answerArray[i] { strike += 1 } // 스트라이크 카운트
            }
            ball = (answerArray.filter { inputArray.contains($0) }).count - strike // 볼 개수 = 교집합의 원소 수 - 스트라이크 개수
            print("\(strike)스트라이크 \(ball)볼")
        }
        else { // 입력이 3자리가 아니거나 숫자가 아닐 경우
            if inputNumber.count != 3 { // 중에 세 자리가 아닌 경우
                print("세자리 숫자를 입력하세요.")
            } else { // 혹은 숫자가 아닌 글자가 포함된 경우
                print("숫자만 입력하세요")
            }
        }
        
        print()
        print("숫자를 입력하세요")
        inputNumber = readLine()!
    }
    print("정답입니다!")

    반복을 실행할 때마다 스트라이크와 볼의 판별 카운트가 초기화 되도록 하기 위해 while문 안에 스트라이크와 볼을 카운트할 변수 strike와 ball을 만들어 초기값을 0으로 설정하였다.

    이후 if문을 사용해 입력한 글자의 개수가 3개인지, 그리고 Int로 형변환이 가능한지 확인해 3글자이면서 숫자만 입력한지 판단하였다.

    만약 맞다면 입력을 한 글자씩 배열에 저장한 뒤, for문을 사용해 정답 배열과 입력 배열의 인덱스가 같을 때 숫자도 같은지 확인하였다. 만약 같다면 위치와 숫자가 모두 같으므로 스트라이크 카운트가 증가할 것이다.

    그리고 볼은 정답 배열과 입력 배열에 같은 요소가 있는지 확인한 뒤 그 수를 세도록 filter와 contains($0), 그리고 count를 사용하였다.

    이렇게 하면 숫자의 위치와 상관없이 입력과 정답의 같은 숫자가 몇 개인지 세는 것이기 때문에 여기서 스트라이크의 숫자를 빼야 볼의 수가 나온다.

    이후 else문에서 입력이 3개가 아니거나 숫자가 아닌 글자가 포함된 경우 문구를 띄우도록 하였고 마지막에 재입력을 받아 while문이 계속 동작하도록 유도하였다.

    정답을 맞추면 while문이 종료되므로 정답을 알리는 문구를 출력하게 하여 코드 작성을 마무리 하였다.

    원하던대로 3자리 숫자가 아니거나 알파벳 문자가 포함 된 경우 안내 문구가 나오고, 세자리 숫자만 입력했을때만 스트라이크와 볼을 카운트 하다가 정답을 맞추면 종료가 되는 것을 확인할 수 있다.


    기능별 클래스를 만들기

    이제 정답을 랜덤으로 출력하게 할 건데, 그전에 여러 기능들, 0이 있는지 체크하는 기능, 숫자로 변환 불가능한 문자가 있는지 체크하는 기능, 랜덤 숫자를 만들어 정답으로 반환하는 기능, 스트라이크와 볼을 판별하는 기능 등을 각각의 클래스로 만들어 분리할 것이다. 그래야 Lv3 ~ Lv6에서 요구하는 기능들을 만들때 편할 것 같기 때문이다.

    영어 실력 이슈로 당장 생각나는 클래스명들은

    • makeAnswer(정답을 만드는 클래스)
    • convertNumber(한 글자씩 문자로 변환해 배열에 저장하는 클래스)
    • checkNumber(숫자인지 체크하는 클래스)
    • checkThree(세글자인지 확인하는 클래스)
    • checkZero(0이 포함되어 있는지 확인하는 클래스)
    • checkDuplication(숫자의 중복이 있는지 체크하는 클래스)
    • checkStrike(스트라이크와 볼을 카운트하는 클래스)

    정도로 정리 할 수 있을 것 같다.


    makeAnswer 클래스

    makeAnswer 클래스는 난수를 생성해 반환하면 되므로

    class makeAnswer { // 정답으로 할 3자리 랜덤 난수 생성 클래스
        func randomAnswer() -> String {
            return String(Int.random(in: 100...999))
        }
    }

    이렇게 작성하였다. return을 String으로 형변환 하여 반환한 것은, 어차피 나중에 문자열로 변환해서 배열에 저장해야 하기 때문에 메인에서 매번 형변환 하는 것보다 반환할 때 형변환 해서 반환하는 게 더 깔끔하다고 생각해서이다.


    convertNumber 클래스

    다음은 convertNumber 클래스이다. 이 클래스는 정답과 입력 받은 숫자를 한 글자씩 배열에 저장할 때 필요한 클래스로, 입력은 String, 반환은 [String] 이다.

    class convertNumber { // 한글자씩 문자로 배열에 저장하는 클래스
        func convertArray(input: String) -> [String] {
            var stringArray: [String] = []
            for char in input {
                stringArray.append(String(char))
            }
            return stringArray
        }
    }

    입력을 받아서 stringArray라는 빈 배열에 한 글자씩 추가한 뒤 stringArray를 반환한다. 이러면 해당 클래스의 인스턴스를 사용하면 입력이나 정답의 숫자들이 한 글자씩 저장된 배열이 반환 될 것이다. 그 반환된 배열로 스트라이크와 볼의 카운트를 하기도 하고, 옳게 된 형식의 입력 또는 정답인지 확인 할 수도 있다.


    여러 check 클래스들

    다음은 checkNumber 클래스이다. 이것은 입력이 Int로 변환 가능한 숫자만 입력하는지 확인할 때 사용할 클래스이다. String으로 입력 받아서 Bool 타입의 반환을 할 것이다.

    class checkNumber { // 입력이 숫자인지 체크할 클래스
        func check(input: String) -> Bool {
            return Int(input) == nil // Int로 변환 가능하면 false, 변환 불가능하면(=문자가 있으면) true 반환
        }
    }

    입력을 Int로 형변환 한 것이 nil인지 확인하여, 변환이 가능하다면 숫자만 입력한 것이므로 false를 반환하도록 하였다. 숫자로 변환 가능할 때 true가 아닌 false로 반환하도록 한 것은, 뒤에 있을 다른 체크 클래스들과 반환 값을 통일해 알고리즘을 짜기 편하게 하려고 한 것이다.

    class checkThree { // 3글자인지 체크하는 클래스
        func check(input: String) -> Bool {
            return input.count != 3 // 3글자 일때 false, 3글자가 아닐때 false 반환
        }
    }
    
    class checkZero { // 숫자에 0이 있는지 체크하는 클래스
        func check(input: [String]) -> Bool {
            return input.contains("0") // 0이 있으면 true 반환, 없으면 false 반환
        }
    }
    
    class checkDuplication { // 중복 있는지 체크하는 클래스
        func check(input: [String]) -> Bool {
            return Set(input).count != input.count // 입력으로 들어온 배열을 카운트한 값과 Set으로 넣고 카운트한 값이 다르면(=중복이 있으면) true 반환, 같으면(=중복이 없으면) false 반환
        }
    }

    나머지 체크 클래스들도 비슷한 방식으로 작성하였다. 굳이 메서드 명을 check로 통일한 것은 코드가 비슷해서 나중에 프로토콜을 작성해 더 가독성 좋은 코드를 작성할 수 있겠다는 생각을 했기 때문이긴 한데, 기간 안에 할 수 있을지는 아직 잘 모르겠다.


    strike, ball 카운트 클래스

    이제 스트라이크와 볼을 카운트하는 클래스이다.

    class checkStrike { // 스트라이크와 볼을 카운트하는 클래스
        func check(answerArray: [String], inputArray: [String]) -> Int {
            var strike = 0
            var ball = 0
            for i in 0..<inputArray.count {
                if inputArray[i] == answerArray[i] { strike += 1 } // 스트라이크 카운트
            }
            ball = (answerArray.filter { inputArray.contains($0) }).count - strike // 교집합의 원소 수 센 다음 스트라이크만큼 뺀 수로 볼 카운트
            print("\(strike)스트라이크 \(ball)볼")
            return strike
        }
    }

     

    이전에 작성한 코드를 거의 그대로 옮겨서 아직은 카운트 하는 클래스 안에 print()까지 있는데 나중에 정리할 것이다.

    스트라이크를 반환하도록 하였는데, 이유는 스트라이크가 3개가 나올 때까지 반복을 시키려고 하기 때문이다. 나중에 print 함수 때문에 이 클래스의 코드를 정리할 때 strike 뿐만 아니라 ball도 같이 반환하게 수정해야 할 것 같다.


    출력 구현부

    다음은 출력 구현부 작성인데,

    /*                  출력 구현 부                 */
    var answer = makeAnswer().randomAnswer() // 정답 숫자
    let convert = convertNumber() // 배열로 변환할 인스턴스
    let CheckNumber = checkNumber() // 숫자인지 체크할 인스턴스
    let CheckZero = checkZero() // 0 있는지 체크할 인스턴스
    let CheckDup = checkDuplication() // 중복 체크할 인스턴스
    let CheckThree = checkThree() // 3글자인지 체크할 인스턴스
    let CheckStrike = checkStrike() // 정답인지 체크할 인스턴스
    
    var answerArray = convert.convertArray(input: answer) // 정답 숫자 문자로 변환 후 배열에 저장
    while CheckZero.check(input: answerArray) || CheckDup.check(input: answerArray) { // 0과 중복 둘 중 하나라도 있으면 정답 재설정
        answer = makeAnswer().randomAnswer()
        answerArray = convert.convertArray(input: answer)
    }
    
    
    print("< 게임을 시작합니다 >")
    print("숫자를 입력하세요")
    
    var inputNumber = readLine()!
    var inputArray = convert.convertArray(input: inputNumber) // 입력 숫자 문자로 변환 후 배열에 저장
    
    var checkInputNumber = CheckNumber.check(input: String(inputNumber)) || CheckZero.check(input: inputArray) || CheckDup.check(input: inputArray) || CheckThree.check(input: String(inputNumber))// 입력에 0과 문자, 중복이 있는지 체크
    
    while checkInputNumber { // 하나라도 해당 되면 반복해서 재설정
        print("숫자만 3자리로 다시 입력해주세요")
        inputNumber = readLine()!
        inputArray = convert.convertArray(input: inputNumber)
        checkInputNumber = CheckNumber.check(input: String(inputNumber)) || CheckZero.check(input: inputArray) || CheckDup.check(input: inputArray) || CheckThree.check(input: String(inputNumber))
    }
    
    var checkStrikeCount = CheckStrike.check(answerArray: answerArray, inputArray: inputArray)
    
    while checkStrikeCount != 3 {
        print()
        print("숫자를 입력하세요")
        inputNumber = readLine()!
        inputArray = convert.convertArray(input: inputNumber) // 다시 배열로 변환
        checkInputNumber = CheckNumber.check(input: String(inputNumber)) || CheckZero.check(input: inputArray) || CheckDup.check(input: inputArray) || CheckThree.check(input: String(inputNumber))// 다시 0과 중복 체크
        checkStrikeCount = CheckStrike.check(answerArray: answerArray, inputArray: inputArray)
    }
    print("정답입니다!")

    처음엔 이렇게 작성했었다. 각 클래스들의 인스턴스를 생성하고, 정답을 생성한 후에,  입력을 받아서 한 글자씩 배열에 저장하고, 그 배열을 통해 입력이 옳은 형식인지 확인한 다음, 틀리다면 형식에 맞을 때까지 재입력, 배열에 재저장 하는 과정을 거치도록 하였다.

    그 후 입력이 항상 옳은 형식으로 들어오게 되면, 스트라이크와 볼을 판별하는 CheckStrike 클래스의 인스턴스를 사용해 스트라이크 수를 반환, 3스트라이크가 아니면 다시 재입력을 받아 검증 한 뒤 CheckStrike를 사용하는 방식이다.


    입력 검증 클래스 새로 만들기

    그런데 입력을 받고, 검증을 하는 코드가 여러 논리 연산자를 사용해서 코드가 긴데, 여러번 반복하다 보니 코드가 지저분해 보였다. 그래서 옳은 입력인지 검증하는 checkCorrectInput 이라는 클래스를 만들어 검증하도록 하기로 했다.

    class checkCorrectInput { // 입력이 제대로 됐는지 확인하는 클래스
        var CheckNumber: checkNumber
        var CheckZero: checkZero
        var CheckDuplication: checkDuplication
        var CheckThree: checkThree
        var ConvertNumber: convertNumber
        
        init() {
            self.CheckNumber = checkNumber()
            self.CheckZero = checkZero()
            self.CheckDuplication = checkDuplication()
            self.CheckThree = checkThree()
            self.ConvertNumber = convertNumber()
        }
        func check(input: String) -> Bool { // 3글자이면서, 0이 없고, 중복도 없고, 숫자만 입력되었을때 false, 아니면 true
            let arr = ConvertNumber.convertArray(input: input)
            return CheckNumber.check(input: input) || CheckZero.check(input: arr) || CheckDuplication.check(input: arr) || CheckThree.check(input: input)
        }
        func repeatInput() -> String {
            print("숫자만 3자리로 다시 입력해주세요")
            var inputNumber = readLine()!
            while check(input: inputNumber) {
                print("숫자만 3자리로 다시 입력해주세요")
                inputNumber = readLine()!
            }
            return inputNumber
        }
    }

    해당 클래스는 다음과 같이 작성하였다. 옳은 입력인지 확인하려면, 위에 작성한 여러 체크 클래스들이 필요하기에 그 클래스들의 인스턴스를 생성해 초기화 하였다.

    그리고 func check를 통해 3글자이면서 0과 중복이 없고, 숫자만 입력 된 모든 조건이 충족될때만 false가 반환되도록 하여 하나라도 틀릴 시 재입력을 받는 while문을 동작하게 하였다.


    마무리

    입력 검증 부분만 클래스를 사용하여 바꾸고 다음과 같이 완성하였다.

    /*                  출력 구현 부                 */
    var answer = makeAnswer().randomAnswer() // 정답 숫자
    let convert = convertNumber() // 배열로 변환할 인스턴스
    let CheckNumber = checkNumber() // 숫자인지 체크할 인스턴스
    let CheckZero = checkZero() // 0 있는지 체크할 인스턴스
    let CheckDup = checkDuplication() // 중복 체크할 인스턴스
    let CheckThree = checkThree() // 3글자인지 체크할 인스턴스
    let CheckStrike = checkStrike() // 스트라이크를 체크할 인스턴스
    var CheckCorrectInput = checkCorrectInput() // 입력이 맞게 되었는지 확인할 인스턴스
    
    var answerArray = convert.convertArray(input: answer) // 정답 숫자 문자로 변환 후 배열에 저장
    while CheckZero.check(input: answerArray) || CheckDup.check(input: answerArray) { // 0과 중복 둘 중 하나라도 있으면 정답 재설정
        answer = makeAnswer().randomAnswer()
        answerArray = convert.convertArray(input: answer)
    }
    
    print("< 게임을 시작합니다 >")
    print("숫자를 입력하세요")
    
    var inputNumber = readLine()!
    while CheckCorrectInput.check(input: inputNumber) { // 입력에 이상 없는지 체크 후 이상 있으면 재입력
        inputNumber = CheckCorrectInput.repeatInput()
    }
    
    var inputArray = convert.convertArray(input: inputNumber) // 비교를 위해 문자로 바꿔 배열 입력
    var checkStrikeCount = CheckStrike.check(answerArray: answerArray, inputArray: inputArray) // 볼&스트라이크 판별
    
    while checkStrikeCount != 3 { // 스트라이크가 3개가 아니면 계속 반복
        print()
        print("숫자를 입력하세요")
        inputNumber = readLine()! // 숫자 재입력
        while CheckCorrectInput.check(input: inputNumber) { // 다시 입력한 숫자에 문제 없는지 체크
            inputNumber = CheckCorrectInput.repeatInput()
        }
        inputArray = convert.convertArray(input: inputNumber) // 재비교를 위해 다시 배열에 저장
        checkStrikeCount = CheckStrike.check(answerArray: answerArray, inputArray: inputArray) // 다시 스트라이크 체크
    }
    print("정답입니다!")

    checkCorrectInput의 인스턴스를 생성하고 while문을 해당 인스턴스의 check 메서드를 호출해 동작하도록 하였고, 재입력을 받는 부분도 repeatInput 메서드를 호출해 받도록 하였다.


    느낀점

    생각보다 많은 기능들이 필요해 비슷하면서도 많은 클래스들을 작성하게 되었다.

    나중을 생각해 최대한 비슷하게 작성하면서 진행하였는데, 그러다보니 오히려 더 헷갈려서 시간이 오래 걸린 부분이 있었다. 다음번에 작성한다면 큰 틀을 먼저 작성하고, 각각의 세부 내용을 작성하는 식으로 하는 게 나을 것 같다.

    그리고 TIL 쓰면서 코드를 다시 쭉 읽어보는데, 논리 연산자를 사용해 입력을 검증하는 부분이 조금 이상하게 된 것 같다. checkCorrectInput의 check 메서드가 입력 값을 검증할 때 or 연산자를 썼는데, 모든 조건을 충족할때만 false 를 반환하고 하나라도 틀리면 true를 반환하려고 했던 것이다. 그래야만 재입력을 받기 위한 while문이 동작할테니까..

    그래서 처음에 각종 체크 클래스들의 반환값도 조건에 맞으면 false를 반환하도록 했는데, 논리 연산을 잘못 생각하다 보니 의도대로 안되고, 그러다 보니  각종 check 클래스들의 반환 값을 될때까지 바꿔대서 결과적으로 왜 되는지 모르겠는데 원하는대로 되긴 되는 코드가 되었다.

    여러 반환 값들을 동시에 생각해서 논리연산을 하려다보니 헷갈려 버려서 매우 단순무식한 방법을 선택한 것이다. 이런 거 말고 의도대로 동작하도록 논리 연산 코드를 수정할 것이다. 

    각종 체크 클래스가 무엇을 기능하기 위해 있는지 생각하고, 무엇을 반환시킬지 정한다음 checkCorrectInput 클래스나 재입력을 받는 while문들도 수정할 것이다.. 내가 원하는 기능이 뭔지 계속 생각하면서 작성하면 어렵지 않을 것이다.

Designed by Tistory.