[TIL / 25.03.11] 숫자 야구 게임을 만들어보았소.

2025. 3. 11. 14:42·Algorithm

1. 구현해야 할 것

 

이번 주 과제로 숫자 야구 게임을 만들어보도록 한다.

설명은 위 이미지를 보면 이해가 될텐데 ,, 어렴풋이 기억이 난다.

 

다만 이런 자료구조를 이용한 문제 풀이는 거의 생소하다보니 미친듯이 삽질했다,,,,,,흑

 

2. 구현 내용

우선 중간중간 계속 코드를 갈아엎었는데 안되는 이유를 알지만 명확히 쓰기는 좀 힘들어서 최대한 문제 해결 쪽에 초점을 맞추다보니 과정에 대한 이미지가 없다,,

 

2.1 전체 코드

import Foundation

struct BaseBallGame {
    func start() {
        var gameHistory = [GameHistory]()
        var gameCount = 0
        while true {
            
            // MARK: - Onboarding
        
            print("환영합니다! 원하시는 번호를 입력해주세요")
            print("1. 게임 시작하기  2. 게임 기록 보기  3. 게임 설명 보기  4. 종료하기")
            
            let mode = readLine()
            
            switch mode {
                
            case "1":
                var repeatedCount = 0
                var myStrikeList = [Int]()
                let answer = makeAnswer()
                
                gameCount += 1
                
                while true {
                    
                    if repeatedCount == 0 { print("< 게임을 시작합니다 >") }
                    
                    // MARK: - 사용자로부터 입력 값 입력 및 유효성 검증
                    print("숫자를 입력하세요")
                    let myNumber = readLine()
                    guard let input = myNumber, let intTypeInput = Int(input), intTypeInput >= 100, intTypeInput <= 999 else {
                        print("올바르지 않은 입력값입니다.")
                        continue
                    }
                    
                    // MARK: - 기존 정답과 인풋에 대한 스트라이크, 볼, 정답 여부 확인 로직
                    let answerString = String(answer)
                    
                    myStrikeList = findStrike(answer: answerString, myNum: input)
                    
                    let ballCount = findBall(answer: answerString, myNum: input, myStrikeList: myStrikeList)
                    
                    if myStrikeList.count == 3 {
                        print("정답입니다!")
                        let history = GameHistory(gameNumber: gameCount, tryCount: repeatedCount)
                        gameHistory.append(history)
                        break
                    } else {
                        repeatedCount += 1
                        print("\(myStrikeList.count) 스트라이크, \(ballCount) 볼")
                    }
                    
                }
                
            case "2":
                print("< 게임 기록 보기 >")
                
                if gameHistory.isEmpty {
                    print("게임을 플레이하신 내역이 없습니다. 게임을 플레이해주세요!\n")
                } else {
                    print("게임 기록을 불러오는 중입니다...")
                    usleep(1500000)
                    
                    gameHistory.forEach { history in
                        history.presentTryCount()
                    }
                    print("\n")
                }
                
            case "3":
                print("===== 숫자 야구 게임 설명 =====")
                print("1. 컴퓨터가 서로 다른 3자리 숫자를 무작위로 생성합니다 (첫 자리는 0이 올 수 없음)")
                print("2. 사용자가 3자리 숫자를 입력합니다")
                print("3. 컴퓨터는 숫자와 위치가 모두 맞으면 '스트라이크', 숫자는 맞지만 위치가 다르면 '볼'로 알려줍니다")
                print("   예) 정답: 874, 입력: 847 → 1스트라이크 2볼")
                print("4. 3개의 숫자를 모두 맞추면(3스트라이크) 게임 승리!")
                print("5. 게임 기록은 '게임 기록 보기' 메뉴에서 확인할 수 있습니다")
                print("=============================")
            case "4":
                print("< 숫자 야구 게임을 종료합니다 >\n")
                return
            default:
                print("올바른 숫자를 입력해주세요!\n")
            }
        }
    }
    
    func makeAnswer() -> Int {
        while true {
            let answer = Int.random(in: 100...999)
            
            let hundredsNum = answer / 100
            let tensNum = (answer - (hundredsNum * 100)) / 10
            let oneNum = (answer - (hundredsNum * 100)) % 10
            
            guard (hundredsNum != tensNum) && (tensNum != oneNum) && (hundredsNum != oneNum) && (hundredsNum != 0) else {
                continue
            }
            
            return answer
        }
    }
    
    func findStrike(answer: String, myNum: String) -> [Int] {
        var strikeList = [Int]()
        
        for i in 0...2 {
            let answerIndex = answer.index(answer.startIndex, offsetBy: i)
            let myNumberIndex = myNum.index(myNum.startIndex, offsetBy: i)
            
            if answer[answerIndex] == myNum[myNumberIndex] {
                strikeList.append(i)
            }
        }
        
        return strikeList
    }
    
    func findBall(answer: String, myNum: String, myStrikeList: [Int]) -> Int {
        var ballCount = 0
        
        for (answerIndex, i) in answer.enumerated() {
            for (myNumIndex, j) in myNum.enumerated() {
                if i == j {
                    if !myStrikeList.contains(myNumIndex) && !myStrikeList.contains(answerIndex) {
                        ballCount += 1
                    }
                }
            }
        }
        
        return ballCount
    }
    
}
import Foundation

struct GameHistory {
    let gameNumber: Int
    let tryCount: Int
    
    func presentTryCount() {
        print("\(gameNumber)번째 게임 : 시도 횟수 - \(tryCount)")
    }
}

 

우선 전체 코드부터 공유하고 그 과정에 대해 써봅니다~

 

2.2 레벨 1, 3

일단 컴퓨터에서 답을 정하도록 설정해줘야됨.

1부터 9까지의 서로 다른 임의의 수 3개임을 3레벨에서 미션으로 정하는데, 여기서 그럼 없는 버전으로 223같은 답으로 하려다가 생각보다 구현이 많이 복잡해져서 시간을 제일 많이 잡아먹음.

 

솔직히 그냥 188같이 같은 숫자가 있는 세 자리의 숫자 자체는 범위 지정해 랜덤 메서드만 돌리면 됨.

let answer = Int.random(in: 100...999)

 

이렇게만 하면 그 안에서 뭐든 되는데, 미리 리스크를 줄이고 갔음.

 

우선 이 컴퓨터가 내린 숫자이기 때문에 정수형이니 그냥 각 단위 숫자를 따로 계산해서 얻어내는 방식으로 했음

func makeAnswer() -> Int {
        while true {
            let answer = Int.random(in: 100...999)
            
            let hundredsNum = answer / 100
            let tensNum = (answer - (hundredsNum * 100)) / 10
            let oneNum = (answer - (hundredsNum * 100)) % 10
            
            guard (hundredsNum != tensNum) && (tensNum != oneNum) && (hundredsNum != oneNum) && (hundredsNum != 0) else {
                continue
            }
            
            return answer
        }
    }

 

각 단위 숫자를 구하고 각 단위 숫자가 다른 숫자들과 아예 같지 않을 때까지 반복문을 돌려 원하는 조건의 답이 나올때까지 반복하도록 함

 

그리고 3레벨에서 말하는 맨 앞자리 0은 애초에 random 파라미터 범위로 100부터 넣도록 설정해주었기에 맨 앞자리는 0이 나올 수가 없음.

 

2.3 레벨 2

요런 가닥인데, 어제 하루종일 붙잡고 중복되는 숫자 맞출라고 로직 짜다가 도저히 답이 안나와서 오늘 풀어보려고 자려고 누웠다가, 손으로 노트에 케이스 직접 그려가면서 정리해보니,,,

 

사실 스트라이크를 전부 미리 빼두고 남은 숫자들끼리만 보면 되네...? (이 쉬운걸 왜 생각못했을까 vs 이게 정말 맞는걸까? 구도로 자아분열함)

 

기존엔 정답측, 내가 쓴 번호 측에 관련한 배열을 따로 선언해준 후, 스트라이크던 볼이던 미리 맞았다면 해당 순서쌍이던 한쪽씩 인덱스를 각 관련 배열에 넣던지 해서 후에 다른 요소랑 매칭되어도 배열에 이미 있다면 카운트가 올라가지 않도록 해야겠다 !! 라고 생각했음.

근데 정말 122 와 272 같은 경우가 생긴다면 미리 인덱스를 한케이스에 관해 넣어놨다고 해도 추가로 생기는 모순이 많았음.

 

어쨋든 스트라이크 우선적으로 확인할 수 있도록 하는 로직이

func findStrike(answer: String, myNum: String) -> [Int] {
        var strikeList = [Int]()
        
        for i in 0...2 {
            let answerIndex = answer.index(answer.startIndex, offsetBy: i)
            let myNumberIndex = myNum.index(myNum.startIndex, offsetBy: i)
            
            if answer[answerIndex] == myNum[myNumberIndex] {
                strikeList.append(i)
            }
        }
        
        return strikeList
    }

 

이 로직을 통해 서로 같은 자리의 값을 비교해 스트라이크인지 비교함

여기서 몰랐던게, String형의 문자열에서 index를 통해 문자에 접근하려면 index(_:offsetBy:) 메서드를 통해 어느 배열에 몇번째 offset으로 접근할건지 정의해줘야 해당 문자를 반환받을 수 있음.

 

그래서 스트라이크인 경우 배열에 넣어둡니다. 해당 스트라이크의 인덱스를 !!

 

func findBall(answer: String, myNum: String, myStrikeList: [Int]) -> Int {
        var ballCount = 0
        
        for (answerIndex, i) in answer.enumerated() {
            for (myNumIndex, j) in myNum.enumerated() {
                if i == j {
                    if !myStrikeList.contains(myNumIndex) && !myStrikeList.contains(answerIndex) {
                        ballCount += 1
                    }
                }
            }
        }
        
        return ballCount
    }

 

그럼 스트라이크를 미리 빼뒀으니 볼을 찾는 작업을 해야하는데, 말했듯 미리 스트라이크를 정의해뒀으니 볼은 각각 대조해서 같은 경우인데 인덱스가 스트라이크쪽에 미리 쓰여지지 않았다면 볼 처리가 된다.

 

앞선 함수들을 종합해 

// MARK: - 기존 정답과 인풋에 대한 스트라이크, 볼, 정답 여부 확인 로직
    let answerString = String(answer)

    myStrikeList = findStrike(answer: answerString, myNum: input)

    let ballCount = findBall(answer: answerString, myNum: input, myStrikeList: myStrikeList)

    if myStrikeList.count == 3 {
        print("정답입니다!")
        let history = GameHistory(gameNumber: gameCount, tryCount: repeatedCount)
        gameHistory.append(history)
        break
    } else {
        repeatedCount += 1
        print("\(myStrikeList.count) 스트라이크, \(ballCount) 볼")
    }

 

이렇게 스트라이크와 볼의 개수를 출력할 수 있게 된다. 사실 이 섹션이 이 게임의 메인임.

 

2.4 레벨 4

 

사실 메인 기능들은 다 구현되었으니 이제 게임을 꾸미는 단계라고 생각이 듭니다~

 

// MARK: - Onboarding
        
    print("환영합니다! 원하시는 번호를 입력해주세요")
    print("1. 게임 시작하기  2. 게임 기록 보기  3. 게임 설명 보기  4. 종료하기")

    let mode = readLine()

    switch mode {

 

이전 사항까지는 while문 하나에만 모든 로직이 들어가 게임 실행에만 초점을 맞췄다면, 이젠 그 게임 시작 자체는 내부 기능 중 하나가 되는 것이고 그 상위로 메뉴를 고를 수 있게 해줘야함!!

 

이렇게 print로 메뉴를 보여주고 readLine으로 어느 모드 혹은 메뉴를 원하는지 입력받은 후 switch문을 통해 해당 메뉴에 대한 처리를 한다.

2.5 레벨 5, 6

 

마무리 짓는 단계. 게임 기록에 대해서도 보여주고 종료 기능까지 추가해주면 끝.

 

case "2":
    print("< 게임 기록 보기 >")

    if gameHistory.isEmpty {
        print("게임을 플레이하신 내역이 없습니다. 게임을 플레이해주세요!\n")
    } else {
        print("게임 기록을 불러오는 중입니다...")
        usleep(1500000)

        gameHistory.forEach { history in
            history.presentTryCount()
        }
        print("\n")
    }

 

struct GameHistory {
    let gameNumber: Int
    let tryCount: Int
    
    func presentTryCount() {
        print("\(gameNumber)번째 게임 : 시도 횟수 - \(tryCount)")
    }
}

 

아까 전체 코드 블럭을 보면 나오지만, 전체적으로 gameCount, gameHistory 관련된 선언을 해줬고 해당 데이터를 관리할 수 있도록 해주는데, 2번을 누르게 되면 저장되어있던 기록을 불러오도록 forEach문을 돌려 print해줄 수 있게 해두었음.

 

다만 아무 기록 없으면 좀 허전하니 임의로 빈 페이지 UI 넣듯이 저렇게 print를 하도록 해놨고, 게임 기록이 있을 때도 바로 띄우기보단 로딩하는 척할 수 있게 대기시간을 중간에 설정해두었음.

 

2.6 그 외 기능 - 게임 설명

이왕 게임을 하는거니 처음 접해본 사람들을 위한 설명 기능이 있으면 좋겠다 싶어, print로 설명을 볼 수 있게 기능 추가를 해주었음

case "3":
    print("===== 숫자 야구 게임 설명 =====")
    print("1. 컴퓨터가 서로 다른 3자리 숫자를 무작위로 생성합니다 (첫 자리는 0이 올 수 없음)")
    print("2. 사용자가 3자리 숫자를 입력합니다")
    print("3. 컴퓨터는 숫자와 위치가 모두 맞으면 '스트라이크', 숫자는 맞지만 위치가 다르면 '볼'로 알려줍니다")
    print("   예) 정답: 874, 입력: 847 → 1스트라이크 2볼")
    print("4. 3개의 숫자를 모두 맞추면(3스트라이크) 게임 승리!")
    print("5. 게임 기록은 '게임 기록 보기' 메뉴에서 확인할 수 있습니다")
    print("=============================")

 

요런식으로 해서 야구 게임 끝!!

 

3. 구현 결과

게임 플레이 내부 (게임 시작하기 선택 시)
게임 기록 보기 선택 시, 아무 기록이 없는 경우.
사용자 입력이 잘못되었을 경우
게임 설명 보기 선택 시


알고리즘 관련 문제들은 쥐약이라 푸는 데 머릿 속으로 데이터들의 흐름이 알면서도 정리가 안되는 현상의 무한 반복이었다..

가만 보면 요 기능은 꽤 쉬울 것 같은데 ? 싶은 부분엔 허점이 굉장히 많고 함정에 빠지기 너무 쉬워서 구현하다가 갈아엎은게 한두번이 아닌,,, 끝나고 나서 보면 방법론 자체만 봤을때 이 쉬운걸 왜 생각못했지? 라고 생각이 드는데, 나중엔 정말 딱보면 쉽네... 하고 방법을 떠올릴 수 있는 실력이 되면 좋겠다는 생각이다.

 

내일부터 본격적으로 알고리즘을 조금씩 하게 되는데 걱정과 함께 성장에 대한 기대도 몰려온다. 드가자~

'Algorithm' 카테고리의 다른 글

[Algorithm / 25.04.04] 프로그래머스 - 피로도  (2) 2025.04.04
[Algorithm / 25.03.31] 프로그래머스 - 카펫  (0) 2025.03.31
[Algorithm / 25.03.21] 달리기 경주  (0) 2025.03.21
[Algorithm / 25.03.20] 카드 뭉치  (2) 2025.03.20
[Algorithm / 25.03.18] 추억 점수  (5) 2025.03.18
'Algorithm' 카테고리의 다른 글
  • [Algorithm / 25.03.31] 프로그래머스 - 카펫
  • [Algorithm / 25.03.21] 달리기 경주
  • [Algorithm / 25.03.20] 카드 뭉치
  • [Algorithm / 25.03.18] 추억 점수
subkyu-ios
subkyu-ios
subkyu-ios 님의 블로그 입니다.
  • subkyu-ios
    subkyu-ios 님의 블로그
    subkyu-ios
  • 전체
    오늘
    어제
    • 분류 전체보기 (47) N
      • iOS (31) N
        • Swift (31) N
      • 내일배움캠프 (7)
      • Git, Github (3)
      • Algorithm (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    본캠프
    algorithm
    github
    프로그래머스
    알고리즘
    내일배움캠프
    UIKit
    트러블슈팅
    stackview
    회고
    til
    최적화
    Swift
    TableView
    ios
    KPT
    협업
    사전캠프
    Wil
    tabman
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
subkyu-ios
[TIL / 25.03.11] 숫자 야구 게임을 만들어보았소.
상단으로

티스토리툴바