[피드백] Task 3) 야구 게임 로직 구현하기

soy·2024년 11월 14일

iOS-Task

목록 보기
5/12

이전 글

야구 게임 로직 구현하기


1. enum사용으로 코드 리팩토링 포인트

기존 코드: 처음 게임 메뉴입력을 받고 input에 따라 바로 switch로 분기처리

피드백: enum을 사용하면 맥락을 드러내고 안전한 코드를 작성할 수 있다.

  • 기존 코드
    • 처음 게임 메뉴입력을 받고 input에 따라 바로 switch로 분기처리
func start() {
    while true {
        print("환영합니다! 원하는 번호를 입력해주세요.")
        print("1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기")
        
        let insertNum = readLine()
        
        switch insertNum {
        case "1":
            gameMode() // 게임 실행
            recordManager.add(record: record) // 게임이 끝난 후, 총 시도 횟수를 add함수로 전달
        case "2":
            recordManager.showRecords() // 게임 진행 상황 호출
        case "3":
            print("< 숫자 야구 게임을 종료합니다. >")
            return  // 게임 종료 후 함수 종료
        default:
            print("올바르지 않은 입력입니다. 1, 2, 3 중에서 선택해주세요.")
        }
    }
}
  • 수정한 코드
    • enum을 사용하여 맥락을 드러내고 안전한 코드 작성 가능
    • enum 생성하며 switch구문 실행
    • guard let 구문을 사용하여 입력 받기 (오류 처리)
enum Mode: String {
    case gameStart = "1"
    case gameRecord = "2"
    case gameExit = "3"
}

func start() {
    while true {
        print("환영합니다! 원하는 번호를 입력해주세요.")
        print("1. 게임 시작하기  2. 게임 기록 보기  3. 종료하기")
        
        guard let insertNum = readLine() else {
            print("프로그램이 종료되었습니다")
            exit(0)
        }
        
        switch Mode(rawValue: insertNum) {
        case .gameStart:
            gameMode() // 게임 실행
            recordManager.add(record: record) // 게임이 끝난 후, 총 시도 횟수를 add함수로 전달
            
        case .gameRecord:
            print("<게임 기록>")
            print(recordManager.showRecords()) // 게임 진행 상황 호출
            
        case .gameExit:
            print("< 숫자 야구 게임을 종료합니다. >")
            return  // 게임 종료 후 함수 종료
            
        default:
            print("올바르지 않은 입력입니다. 1, 2, 3 중에서 선택해주세요.")
        }
    }
}

2. makeAnswer함수 리팩토링

  • 기존 코드
//정답 생성 함수
func makeAnswer() {
    var numArr = [0,1,2,3,4,5,6,7,8,9] // 정답 숫자를 만들 때 쓰이는 배열
    randomNums = [] //초기화
    
    for i in 0...2 {
        if i == 0 { // 1번째 정답 숫자 생성
            // 0을 제외한 숫자에서 선택하도록 설정
            let nonZeroNumbers = numArr.filter { $0 != 0 }
            if let num = nonZeroNumbers.randomElement() {
                randomNums.append(num)
                numArr.remove(at: numArr.firstIndex(of: num)!)
            }
        } else { // 2,3 번째 정답 숫자 생성
            if let num = numArr.randomElement() {
                randomNums.append(num)
                numArr.remove(at: numArr.firstIndex(of: num)!)
            }
        }
    }
}
  • 수정 받은 코드
    • 내가 작성한 코드와 비교하면.. 이렇게 간단하게 작성할 수 있구나 싶다 ㅜㅜ 코드를 더 효율적이고 깔끔하게 작성하는 방법을 고민해봐야겠다 , , ,
func makeAnswer() {
    answer = [Int.random(in: 1..<10)]
    while answer.count < 3 {
        let number = Int.random(in: 0..<10)
        guard !answer.contains(number) else { continue }
        answer.append(number)
    }
}

3. 유저에게 명확하게 무엇이 잘못 됐는지 알려주기

  • 기존 코드
    • 조건이 각각 다르지만 동일한 오류 문구를 출력하고 있다.
    • 조건 하나 하나 오류 문구를 출력하면 너무 장황해져서 생략했는데, 사실 사용자 입장에서는 '왜 입력이 안되는지'에 대해서 알 수 있는게 훨씬 좋다!
guard input.count == 3, input[0] != 0, Set(input).count == 3 else {
    print("올바르지 않은 입력 값 입니다.")
    continue
}
  • 수정한 코드
    • 무슨 입력이 어떻게 잘못되었는지를 알려줄 수 있도록 바꾸었다.
// 숫자 입력 및 유효성 검사
if let line = readLine() {
    // 입력된 문자열을 숫자 배열로 변환
    input = line.split(separator: "").compactMap { Int($0) }

    guard input.count == 3 else {
        print("올바르지 않은 입력 값 입니다. 3자리 숫자를 입력하세요.")
        continue
    }

    guard input[0] != 0 else {
        print("올바르지 않은 입력 값 입니다. 첫번째 숫자는 0이 될 수 없습니다.")
        continue
    }

    guard Set(input).count == 3 else {
        print("올바르지 않은 입력 값 입니다. 중복된 숫자는 입력될 수 없습니다.")
        continue
    }
}

4. 코드구조

피드백 내용:
BaseballGame클래스가 많은 책임을 가지고 있어요.
말씀해주신 것처럼 힌트 계산뿐만 아니라 정답을 만들고 인풋을 파싱하는 등의 책임을 다른 컴포넌트로 분리 할 수 있습니다.

  • 수정한 코드
    class는 따로 생성하지 않고, BaseballGame클래스 내부에서 gameMode가 맡았던 사용자 입력, 힌트 생성, 힌트 출력 역할을 함수로 각각 분리하였다.

1. gameMode 함수: 사용자 입력 및 게임 종료 조건 확인
2. makeHintNumber 함수: 스트라이크 및 볼 계산
3. printHint 함수: 게임 결과 출력

// 스트라이크 및 볼 계산
func makeHintNumber() -> (Int, Int) {
    var strike = 0
    var ball = 0

    for i in 0..<input.count {
        if input[i] == randomNums[i] { //스트라이크 계산
            strike += 1
        } else if randomNums.contains(input[i]) { //볼 계산
            ball += 1
        }
    }
    
    return (strike, ball)
}

// 게임 결과 출력
func printHint(strike: Int, ball: Int) -> String {
    var hint: String = ""
    
    //힌트 출력
    if strike == strikeCount {
        hint = "정답입니다!"
    } else if strike == minCount && ball == minCount {
        hint = "Nothing"
    } else {
        hint = "\(strike) Strike \(ball) Ball"
    }
    return hint
}
//gameMode함수 하단부에 makeHintNumber함수, printHint함수 호출
let (strike, ball) = makeHintNumber() //스트라이크, 볼 계산
            
print(printHint(strike: strike, ball: ball)) //결과 출력
            
if strike == strikeCount { // 게임 종료 조건
	return
}

피드백 내용:
RecordManager도 생각해보면 조금 모호한 면이 있습니다.
add함수는 게임의 기록을 저장하는 데 showRecords는 저장된 기록을 불러오고(load) console에 보여주고 있어요.
게임의 기록을 저장하고 불러오는 것에 한정하고 console에 보여주는 책임을 다른 컴포넌트에게 부여하면 책임을 명확히 분리할 수 있어요.

  • 기존 코드 (RecordManager.swift)
// 저장된 게임 기록 출력
func showRecords() {
    if records.isEmpty { //배열이 비었을 경우
        print("저장된 게임 기록이 없습니다.")
        
    } else { //게임 기록 출력
        print("게임 기록:")
        for (index, record) in records.enumerated() {
            print("게임 \(index + 1): \(record)회") //index의 숫자보다 +1회하면 게임을 한 횟수가 됨!
        }
    }
}

//BaseballGame.swift의 func start()
case "2":
	RecordManager.showRecords() // 게임 진행 상황을 RecordManager에서 출력
  • 수정한 코드
    • RecordManager 리팩토링 및 기능 분리 진행
    • 기존 코드에서 게임 결과가 출력되는 부분을 바꾸었다.
// 저장된 게임 기록 출력
func showRecords() -> String {
    var recordResult: String = ""
    
    if records.isEmpty { //배열이 비었을 경우
        recordResult = "저장된 게임 기록이 없습니다."
        
    } else {
        for (index, record) in records.enumerated() {
            recordResult = "게임 \(index + 1): \(record)회" //index의 숫자보다 +1회하면 게임을 한 횟수가 됨!
        }
    }
    return recordResult // 게임 기록을 반환
}

//BaseballGame.swift의 func start()
case "2": // 게임 진행 상황 호출
	print(recordManager.showRecords()) //출력

5. Constant를 변수로 만들기

피드백 내용:
if strike == 3 처럼 3을 직접 비교하는 걸 Magic Number를 사용한다고 말해요.
let maxStrike = 3 또는 이름공간을 만들어서 상수를 따로 관리하면 맥락이 드러나고 정책이 변했을 때(게임의 규칙이 변했을 때) 코드로 대응하기 용이해집니다.

  • 수정한 코드
    • Magic Number를 없애고, 클래스 상단에 strikeCount와 minCount 상수를 선언하여 게임 결과 판정을 진행한다.
class BaseballGame {
    var input : [Int] = [] //사용자가 입력할 숫자 3자리를 담는 배열
    var randomNums : [Int] = [] // 정답 숫자 3자리를 담는 배열
    
    let strikeCount = 3 //스트라이크 3개면 정답
    let minCount = 0

6. Strike, Ball 계산 조건

  • 수정 받은 코드

    • guard 구문을 사용하여 수정된 코드를 전달 받았다.
for (idx, inputNumber) in input.enumerated() where answer.contains(inputNumber) {
    guard inputNumber == answer[idx] else {
        ball += 1
        continue
    }
    strike += 1
}
  • 수정한 코드
    • 피드백 대신 이전에 같이 코드 리뷰하다가 if-continue를 strike, ball 각각 쓰기 보단 if-else if로 처리하면 어떻냐는 조언을 받아서 그걸로 수정해보았다!
for i in 0..<input.count {
    //스트라이크 계산
    if input[i] == randomNums[i] {
        strike += 1
    } else if randomNums.contains(input[i]) { // else if 사용
        ball += 1
    }
}

7. 커밋 작업 단위

피드백 내용:
커밋의 작업단위가 너무 큽니다😭
[FEAT] 야구 게임 기능 추가를 보면 게임의 핵심 기능 작성을 하나의 커밋에 모두 하셨어요.
입력 유효성 검사, 정답 생성, 스트라이크 계산, 볼 계산 등이 모두 작업단위로 분리될 수 있어보입니다.
코드를 작성하기 전에 무엇을 만들어야 하는 지 리스트를 작성해보면 작업단위를 나누는데 도움이 되기도 합니다.

이 부분은 아직 노력 중.. 사실 피드백 받고 수정한 과제 또한.. 한 번에 다 수정해버려서 ㅜㅜ..

앞으로 나의 룰은 피드백 받은대로
1. 작업 순서 미리 정하기
2. 한 작업 끝날 때마다 커밋하기 (습관 들이기)

피드백 받고 수정할 때에도,
수정 사항 정리 후 한 항목 수정 완료시 마다 커밋하기!

작은 단위로! 자주 커밋하자!

profile
soysoisoyysauce~

2개의 댓글

comment-user-thumbnail
2024년 11월 14일

저도 마음속에 커밋해주세요.

1개의 답글