스파르타 코딩 클럽 - iOS 스타터 6기/본 캠프

20. 스파르타 코딩 클럽 - 야구 게임 만들기 Lv2 ~ Lv6

seongpil Heo 2025. 3. 21. 01:17

 ⚾️ 야구 게임 만들기 Lv2 ~ Lv6

숫자 야구 게임은 두 명이 즐길 수 있는 추리 게임으로, 상대방이 설정한 3자리의 숫자를 맞히는 것이 목표입니다. 각 자리의 숫자와 위치가 모두 맞으면 '스트라이크', 숫자만 맞고 위치가 다르면 '볼'로 판정됩니다. 예를 들어, 상대방의 숫자가 123일 때 132를 추리하면 1 스트라이크 2 볼이 됩니다. 이러한 힌트를 활용하여 상대방의 숫자를 추리해 나가는 게임입니다.

 📝 코드 구현

[ Lv 2 ]

  • 정답을 맞히기 위해 3 자릿수를 입력하고 힌트를 받습니다
  • 힌트는 야구용어인 스트라이크 입니다
  • 같은 자리에 같은 숫자가 있는 경우 스트라이크, 다른 자리에 숫자가 있는 경우 입니다
    • ex) 정답 : 456인 경우
    • 435를 입력한 경우 → 1 스트라이크 1 볼
    • 357을 입력한 경우 → 1 스트라이크
    • 678을 입력한 경우 → 1 볼
    • 123을 입력한 경우 → Nothing
  • 만약 올바르지 않은 입력값에 대해서는 오류 문구를 보여주세요

[ 실행 예시 ]

< 게임을 시작합니다 >
숫자를 입력하세요
435
1스트라이크 1볼

숫자를 입력하세요
357
1스트라이크

숫자를 입력하세요
123
Nothing

숫자를 입력하세요
dfg // 세 자리 숫자가 아니어서 올바르지 않은 입력값
올바르지 않은 입력값입니다

숫자를 입력하세요
199 // 9가 두번 사용되어 올바르지 않은 입력값
올바르지 않은 입력값입니다

숫자를 입력하세요
103 // 0이 사용되어 올바르지 않은 입력값
올바르지 않은 입력값입니다

숫자를 입력하세요
456
정답입니다!

 

[ Lv 2 코드 - BaseballNumberChecker.swift ]

//
//  BaseballNumberChecker.swift
//  BaseballGame
//
//  Created by 허성필 on 3/20/25.
//

import Foundation

class BaseballNumberChecker {
    // 중복된 값을 확인하는 함수
    func alreadyHasNumber(_ number: Int) -> Bool {
        let digits = String(number)
        var already = Set<Character>()
        
        for digit in digits {
            if already.contains(digit) { // 중복일 때
                return true
            }
            already.insert(digit) // 중복이 아닐 때 Set에 저장
        }
        return false
    }
    
    // 정답과 사용자가 입력한 값을 비교하는 함수
    func compareNumber(_ answer: Int, _ inputNumber: Int) -> Bool {
        // [x, y, z] 형태로 변경
        let answerNumber = Array(String(answer)).map { Int(String($0))! }
        let userNumber = Array(String(inputNumber)).map { Int(String($0))! }
        
        // 스트라이크와 볼 판별
        let strikeCount = (0..<3).filter { answerNumber[$0] == userNumber[$0] }.count
        let ballCount = (0..<3).filter { answerNumber.contains(userNumber[$0]) && answerNumber[$0] != userNumber[$0] }.count

        if strikeCount == 3 {
            print("정답입니다!\n")
            return true
        } else if strikeCount == 2 {
            print("2스트라이크\n")
        } else if strikeCount == 1 {
            if ballCount == 2 {
                print("1스트라이크 2볼\n")
            } else if ballCount == 1 {
                print("1스트라이크 1볼\n")
            } else {
                print("1스트라이크\n")
            }
        } else {
            if ballCount == 3 {
                print("3볼\n")
            } else if ballCount == 2 {
                print("2볼\n")
            } else if ballCount == 1 {
                print("1볼\n")
            } else {
                print("Nothing\n")
            }
        }
        return false
    }
}

 

[ Lv 3 ]

  • 정답이 되는 숫자를 0에서 9까지의 서로 다른 3자리의 숫자로 바꿔주세요
  • 맨 앞자리에 0이 오는 것은 불가능합니다

[ Lv 3 코드 - MakeAnswer.swift ]

//
//  MakeAnswer.swift
//  BaseballGame
//
//  Created by 허성필 on 3/20/25.
//

import Foundation

class Number {
    // 세자리 랜덤 숫자를 생성하는 함수
    func makeNumber() -> Int {
        var number = Set<Int>()
        
        repeat {
            number = Set<Int>() // 중복 방지를 위한 Set 사용
            
            while number.count < 3 {
                let randomNum = Int.random(in: 0...9)
                number.insert(randomNum) // 중복이면 추가되지 않음
            }
        } while number.first == 0 // 첫 번째 숫자가 0이면 다시 만들기
        
        let uniqueNumbers = Array(number) // Set을 Array로 변환
        
        // ?? 연산자를 사용하여 안전하게 옵셔널 값 사용
        return Int("\(uniqueNumbers[0])\(uniqueNumbers[1])\(uniqueNumbers[2])") ?? 0
    }
}

 

[ Lv 4 ]

  • 프로그램 시작할 때 안내문구를 보여주세요

[ 실행 예시 ]

// 예시
환영합니다! 원하시는 번호를 입력해주세요
1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기
// 예시
환영합니다! 원하시는 번호를 입력해주세요
1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기
1 // 1번 게임 시작하기 입력

< 게임을 시작합니다 >
숫자를 입력하세요
.
.
.

 

[ Lv 4 코드 - BaseballGame.swift ]

class BaseballGame {
    private let checker = BaseballNumberChecker()
    private let number = Number()
    private let recordManager = RecordManager()
    
    func start() {
        while true {
            print("환영합니다! 원하시는 번호를 입력해주세요")
            print("1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기")
            
            guard let inputString = readLine(),
                  let inputMenu = Int(inputString) else {
                print("올바른 숫자를 입력해주세요")
                break
            }
            
            switch inputMenu {
            case 1:
                print("\n< 게임을 시작합니다 >")
                let count = startGame()
                // return된 count를 recordManager.add 함수로 전달하여 시도 횟수 저장
                recordManager.add(count)
                continue
            case 2:
                recordManager.showRecords() // 시도 횟수 출력
                continue
            case 3:
                print("\n< 숫자 야구 게임을 종료합니다 >")
            default:
                print("올바를 숫자를 입력해주세요!\n")
                continue
            }
            break
            
        } // while문 끝
        
    } // start() 끝
}

 

[ Lv 5 ]

  • 2번 게임 기록 보기의 경우 완료한 게임들에 대해 시도 횟수를 보여줍니다

[ 실행 예시 ]

// 예시
환영합니다! 원하시는 번호를 입력해주세요
1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기
2 // 2번 게임 기록 보기 입력

< 게임 기록 보기 >
1번째 게임 : 시도 횟수 - 14
2번째 게임 : 시도 횟수 - 9
3번째 게임 : 시도 횟수 - 12
.
.
.

 

[ Lv 5 코드 - BaseballGame.swift ]

switch inputMenu {
    case 1:
        print("\n< 게임을 시작합니다 >")
        let count = startGame()
        // return된 count를 recordManager.add 함수로 전달하여 시도 횟수 저장
        recordManager.add(count)
        continue
    case 2:
        recordManager.showRecords() // 시도 횟수 출력
        continue
    case 3:
        print("\n< 숫자 야구 게임을 종료합니다 >")
    default:
        print("올바를 숫자를 입력해주세요!\n")
        continue
    }
    break

 

[ Lv 5 코드 - RecordManager.swift ]

//
//  RecordManager.swift
//  BaseballGame
//
//  Created by 허성필 on 3/20/25.
//

import Foundation

class RecordManager {
    var trialCount: [Int] = []
    
    func add (_ count:Int){
        trialCount.append(count)
    }
    
    func showRecords() {
        print("\n< 게임 기록 보기 >")
        for i in 0...trialCount.count-1 {
            print("\(i+1)번째 게임 : 시도 횟수 - \(trialCount[i])")
        }
        print("")
    }
}

 

[ Lv 6 ]

  • 3번 종료하기의 경우 프로그램이 종료됩니다
  • 이전의 게임 기록들도 초기화됩니다
  • 1, 2, 3 이외의 입력값에 대해서는 오류 메시지를 보여주세요

[ 실행 예시 ]

// 예시
환영합니다! 원하시는 번호를 입력해주세요
1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기
3 // 3번 종료하기 입력

< 숫자 야구 게임을 종료합니다 >

// 예시
환영합니다! 원하시는 번호를 입력해주세요
1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기
4

올바른 숫자를 입력해주세요!

 

[ Lv 6 코드 - RecordManager.swift ]

func start() {
        while true {
            print("환영합니다! 원하시는 번호를 입력해주세요")
            print("1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기")
            
            guard let inputString = readLine(),
                  let inputMenu = Int(inputString) else {
                print("올바른 숫자를 입력해주세요")
                break
            }
            
            switch inputMenu {
            case 1:
                print("\n< 게임을 시작합니다 >")
                let count = startGame()
                // return된 count를 recordManager.add 함수로 전달하여 시도 횟수 저장
                recordManager.add(count)
                continue
            case 2:
                recordManager.showRecords() // 시도 횟수 출력
                continue
            case 3:
                print("\n< 숫자 야구 게임을 종료합니다 >")
            default:
                print("올바를 숫자를 입력해주세요!\n")
                continue
            }
            break
            
        } // while문 끝
        
    } // start() 끝
}

⚠️ 트러블 슈팅 1

Lv 4 문제에서 switch문에서 case 1, case 2 문에 continue를 작성하지 않아서

사용자가 3을 입력해도 게임이 종료되지 않던 문제가 있었다.

 

swift switch 문에서는 break를 명시적으로 작성하지 않아도 암시적 break 동작을 하기 때문에

case 3에서 따로 break를 작성하지 않고, case 1, case 2에 continue를 작성하여 

사용자의 입력값이 3이 아닐 때에는 while문을 계속 반복하도록 수정했다.

⚠️ 트러블 슈팅 2

Lv 5 문제에서 게임의 기록을 저장하고 표시해 주는 함수를 만들 때

로직을 구현하는데 시간이 생각보다 많이 걸렸다

 

BaseballGame.swift의 코드 중 startGame() 함수를 반환타입 없음에서 Int 형 반환타입으로 변경하고

함수 내부에 trialCount 변수를 추가하여 시도 횟수를 저장하고 정답을 맞히면 누적된 trialCount 값을 return 하도록 했다

trialCount 값을 리턴 받으면 이용하여 배열에 값을 저장한다.

 

showRecord() 함수를 통해 trialCount.count만큼 for문을 돌려서 게임 기록을 출력하도록 구현했다.


 ✓ TIL

이번 2주 차 심화 개인 과제를 만들어 보면서 GitHub에 코드를 올릴 때도 branch를 새로 생성해서 commit / push 한 뒤

Pull Request 하는 과정을 수행해 봤는데 main으로 작업할 때 보다 안전하게 코드를 관리할 수 있었다.

 

또한 이전 과제에서는 main.swift 파일에서 모든 클래스와 함수들을 사용했었는데

이번 과제에서는 파일 분리를 하여 여러 개의 .swift 파일을 만들고 코드들을 나눠서 관리하여 보았다.

이러한 방법을 사용하니 가독성이 좋아지고, 클래스들의 유지보수가 쉬워졌다.

 

이번 과제를 통해 클래스를 인스턴스화하는 방법에 대해 조금 더 이해할 수 있었다.

또한 .contains / .map 같은 유용한 함수들을 배우는 기회가 되었다.


 📝 전체 코드

[ main.swift - 시작 코드 ]

//
//  main.swift
//  BaseballGame
//
//  Created by 허성필 on 3/19/25.
//

let game = BaseballGame()
game.start() // BaseballGame 인스턴스를 만들고 start 함수를 구현하기

 

[ BaseballGame.swift - 사용자 입력값, 게임 메뉴 ]

//
//  BaseballGame.swift
//  BaseballGame
//
//  Created by 허성필 on 3/20/25.
//

import Foundation

// BaseballGame.swift 파일 생성
class BaseballGame {
    private let checker = BaseballNumberChecker()
    private let number = Number()
    private let recordManager = RecordManager()
    
    func start() {
        while true {
            print("환영합니다! 원하시는 번호를 입력해주세요")
            print("1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기")
            
            guard let inputString = readLine(),
                  let inputMenu = Int(inputString) else {
                print("올바른 숫자를 입력해주세요")
                break
            }
            
            switch inputMenu {
            case 1:
                print("\n< 게임을 시작합니다 >")
                let count = startGame()
                // return된 count를 recordManager.add 함수로 전달하여 시도 횟수 저장
                recordManager.add(count)
                continue
            case 2:
                recordManager.showRecords() // 시도 횟수 출력
                continue
            case 3:
                print("\n< 숫자 야구 게임을 종료합니다 >")
            default:
                print("올바를 숫자를 입력해주세요!\n")
                continue
            }
            break
            
        } // while문 끝
        
    } // start() 끝
    
    func startGame() -> Int {
        let answer = number.makeNumber() // 정답을 만드는 함수
        var trialCount = 1
        
        while true {
            // 1. 유저에게 입력값을 받음
            print("숫자를 입력하세요")
            
            // 2. 정수로 변환되지 않는 경우 반복문 처음으로 돌아가기
            // guard 를 사용하여 옵셔널 바인딩 처리
            guard let inputString2 = readLine(),
                  let inputNumber = Int(inputString2) else {
                print("세자리 정수를 입력해주세요")
                continue
            }
            
            print("입력한 숫자 : \(inputNumber)")
            
            // 3. 세자리가 아니거나, 0을 가지거나 특정 숫자가 두번 사용된 경우 반복문 처음으로 돌아가기
            if String(inputNumber).count != 3  { // 숫자가 세자리인지 검사
                print("\n올바르지 않은 입력값입니다")
            // } else if String(inputNumber).contains("0") { // 입력값에 0 포함 검사
            //  print("숫자에 0이 포함되어 있습니다.")
            } else if checker.alreadyHasNumber(inputNumber) { // 중복 숫자 검사 함수
                print("\n중복된 입력값이 있습니다")
            } else {
                // 4. 정답과 유저의 입력값을 비교하여 스트라이크/볼을 출력하기
                // 만약 정답이라면 break 호출하여 반복문 탈출
                if checker.compareNumber(answer, inputNumber) {
                    break
                }
            }
            trialCount += 1
        }
        return trialCount
    }
}

 

[ MakeAnswer.swift - 랜덤 3자리 숫자 생성 ]

//
//  MakeAnswer.swift
//  BaseballGame
//
//  Created by 허성필 on 3/20/25.
//

import Foundation

class Number {
    // 세자리 랜덤 숫자를 생성하는 함수
    func makeNumber() -> Int {
        var number = Set<Int>()
        
        repeat {
            number = Set<Int>() // 중복 방지를 위한 Set 사용
            
            while number.count < 3 {
                let randomNum = Int.random(in: 0...9)
                number.insert(randomNum) // 중복이면 추가되지 않음
            }
        } while number.first == 0 // 첫 번째 숫자가 0이면 다시 만들기
        
        let uniqueNumbers = Array(number) // Set을 Array로 변환
        
        // ?? 연산자를 사용하여 안전하게 옵셔널 값 사용
        return Int("\(uniqueNumbers[0])\(uniqueNumbers[1])\(uniqueNumbers[2])") ?? 0
    }
}

 

[ BaseballNumberChecker.swift - 스트라이크, 볼 판별 ]

//
//  BaseballNumberChecker.swift
//  BaseballGame
//
//  Created by 허성필 on 3/20/25.
//

import Foundation

class BaseballNumberChecker {
    // 중복된 값을 확인하는 함수
    func alreadyHasNumber(_ number: Int) -> Bool {
        let digits = String(number)
        var already = Set<Character>()
        
        for digit in digits {
            if already.contains(digit) { // 중복일 때
                return true
            }
            already.insert(digit) // 중복이 아닐 때 Set에 저장
        }
        return false
    }
    
    // 정답과 사용자가 입력한 값을 비교하는 함수
    func compareNumber(_ answer: Int, _ inputNumber: Int) -> Bool {
        // [x, y, z] 형태로 변경
        let answerNumber = Array(String(answer)).map { Int(String($0))! }
        let userNumber = Array(String(inputNumber)).map { Int(String($0))! }
        
        // 스트라이크와 볼 판별
        let strikeCount = (0..<3).filter { answerNumber[$0] == userNumber[$0] }.count
        let ballCount = (0..<3).filter { answerNumber.contains(userNumber[$0]) && answerNumber[$0] != userNumber[$0] }.count

        if strikeCount == 3 {
            print("정답입니다!\n")
            return true
        } else if strikeCount == 2 {
            print("2스트라이크\n")
        } else if strikeCount == 1 {
            if ballCount == 2 {
                print("1스트라이크 2볼\n")
            } else if ballCount == 1 {
                print("1스트라이크 1볼\n")
            } else {
                print("1스트라이크\n")
            }
        } else {
            if ballCount == 3 {
                print("3볼\n")
            } else if ballCount == 2 {
                print("2볼\n")
            } else if ballCount == 1 {
                print("1볼\n")
            } else {
                print("Nothing\n")
            }
        }
        return false
    }
}

 

[ RecordManager.swift - 시도 횟수 저장 ]

//
//  RecordManager.swift
//  BaseballGame
//
//  Created by 허성필 on 3/20/25.
//

import Foundation

class RecordManager {
    var trialCount: [Int] = []
    
    func add (_ count:Int){
        trialCount.append(count)
    }
    
    func showRecords() {
        print("\n< 게임 기록 보기 >")
        for i in 0...trialCount.count-1 {
            print("\(i+1)번째 게임 : 시도 횟수 - \(trialCount[i])")
        }
        print("")
    }
}

💡 구현 사진