From 58f4179e4d4c12a549a55ec3923f535a61c5fb09 Mon Sep 17 00:00:00 2001 From: Bambu Date: Thu, 15 Jan 2026 15:29:05 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Refactor:=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20-=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EC=97=B4?= =?UTF-8?q?=EA=B1=B0=ED=98=95=20/=20=EC=A0=95=EB=8B=B5=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=95=A8=EC=88=98=20/=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <문자열 열거형> - correct, nothing 케이스 추가 - checkAnswer() 분기 처리 수정 - shuffle 활용하여 반복문 제거 - switch문 삭제, Menu 타입으로 바로 반환 - 입력값 공백 삭제 기능 함수 모듈화 --- BaseballGame/BaseballGame/Game.swift | 66 +++++++++++--------------- BaseballGame/BaseballGame/Helper.swift | 7 +-- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/BaseballGame/BaseballGame/Game.swift b/BaseballGame/BaseballGame/Game.swift index e5f557f..6045caf 100644 --- a/BaseballGame/BaseballGame/Game.swift +++ b/BaseballGame/BaseballGame/Game.swift @@ -41,30 +41,14 @@ class BaseballGame { print(GameMessage.welcome) let condition = ["1", "2", "3"] - var menu = "" - - // 메뉴 입력 - var input = readLine() ?? "" - // 입력문 공백 삭제 - menu = input.replacingOccurrences(of: " ", with: "") + var menu = inputWithNoSpace() // 입력문 유효성 검사 while !condition.contains(menu) { print(GameMessage.invalidInput, GameMessage.selectMenuExample) - input = readLine() ?? "" - menu = input.replacingOccurrences(of: " ", with: "") - } - - switch menu { - case Menu.play.rawValue: - return .play - case Menu.record.rawValue: - return .record - case Menu.exit.rawValue: - return .exit - default: - return nil + menu = inputWithNoSpace() // 입력값 공백 제거 함수 확장에 구현 } + return Menu(rawValue: menu) } //MARK: 게임 플레이 함수 @@ -88,20 +72,10 @@ class BaseballGame { isCorrect = false answer = [] - for _ in 0...2 { - // 정답 첫 번째 숫자일 경우 - if answer.isEmpty { - let num = Int.random(in: 1...9) - answer.append(num) - } else { - var num = Int.random(in: 0...9) - // 정답에 포함되어있다면 num 재생성 - while answer.contains(num) { - num = Int.random(in: 0...9) - } - answer.append(num) - } - } + var num = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // 정답 숫자 후보 + num.shuffle() // 숫자 배열 섞기 + answer = num[0] != 0 ? Array(num[0...2]) : Array(num[1...3]) // 첫 번째 숫자가 0일 경우 예외 처리 + debugPrint("정답: \(answer)") } @@ -134,17 +108,23 @@ class BaseballGame { // 힌트 초기화 var hint: (strike: Int, ball: Int) = (0, 0) // 힌트 설정(스트라이크, 볼) - userAnswer.enumerated().forEach { - if answer[$0.offset] == $0.element { + for (i, element) in userAnswer.enumerated(){ + if answer[i] == element { hint.strike += 1 - } else if answer.contains($0.element) { + } else if answer.contains(element) { hint.ball += 1 } } // 힌트에 따른 분기 처리 - print(GameMessage.getHint(for: hint.strike, hint.ball)) - if hint.strike == 3 { isCorrect = true } + if hint.strike == 3 { + isCorrect = true + print(GameMessage.correct) + } else if hint.strike == 0 && hint.ball == 0 { + print(GameMessage.nothing) + } else { + print(GameMessage.getHint(for: hint.strike, hint.ball)) + } } //MARK: 게임 기록 조회 함수 @@ -174,3 +154,13 @@ class BaseballGame { } } + +//MARK: 부가 기능 구현부 +extension BaseballGame { + // 입력값 공백 제거 + func inputWithNoSpace() -> String { + var input = readLine() ?? "" + input = input.trimmingCharacters(in: .whitespacesAndNewlines) + return input + } +} diff --git a/BaseballGame/BaseballGame/Helper.swift b/BaseballGame/BaseballGame/Helper.swift index c2b205b..9b1cd7d 100644 --- a/BaseballGame/BaseballGame/Helper.swift +++ b/BaseballGame/BaseballGame/Helper.swift @@ -23,11 +23,12 @@ enum GameMessage { static let invalidInput = "⚠️ 유효하지 않은 입력입니다." static let duplicateInput = "⚠️ 중복 숫자 입력입니다." + + static let correct = "🎉 정답입니다! 🎉\n" + static let nothing = "❌ Nothing\n" static func getHint(for strike: Int, _ ball: Int) -> String { - return strike == 3 ? "🎉 정답입니다! 🎉\n" - : strike == 0 && ball == 0 ? "❌ Nothing\n" : - "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" + return "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" } static func getRecord(for game: Int, attempt: Int) -> String { From 8237a3365964670c73ecdf0c31a0f264d6610855 Mon Sep 17 00:00:00 2001 From: Bambu Date: Thu, 15 Jan 2026 22:23:12 +0900 Subject: [PATCH 02/10] =?UTF-8?q?Refactor:=20=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=95=A8=EC=88=98,=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=ED=99=94=20=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/BaseballGameManager.swift | 45 +++++++ BaseballGame/BaseballGame/Game.swift | 120 +++++++----------- BaseballGame/BaseballGame/Helper.swift | 29 ----- .../BaseballGame/Model/GameMessage.swift | 37 ++++++ 4 files changed, 127 insertions(+), 104 deletions(-) create mode 100644 BaseballGame/BaseballGame/Controller/BaseballGameManager.swift create mode 100644 BaseballGame/BaseballGame/Model/GameMessage.swift diff --git a/BaseballGame/BaseballGame/Controller/BaseballGameManager.swift b/BaseballGame/BaseballGame/Controller/BaseballGameManager.swift new file mode 100644 index 0000000..2366807 --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/BaseballGameManager.swift @@ -0,0 +1,45 @@ +// +// BaseballGameManager.swift +// BaseballGame +// +// Created by 변예린 on 1/15/26. +// + +import Foundation + +struct BaseballGameManager { + //TODO: 환영 인사 / 메뉴 문자열 구분 필요 + /// 게임 시작 화면에서 메뉴를 선택하는 함수입니다. + func selectMenu() -> Menu? { + while true { + print(GameMessage.welcome) // 환영 인사 + let input = (readLine() ?? "").trimmingCharacters(in: .whitespacesAndNewlines) // 유저 입력값 + + if Menu(rawValue: input) != nil { + return Menu(rawValue: input) // 유효한 입력값일 경우 Menu 타입 반환 + } else { + print(GameMessage.invalidInput, GameMessage.selectMenuExample) // 에러 메세지 출력 + } + } + return nil + } + + //TODO: 반환값 타입 정하기 + // 정답 & 유저 입력 비교 함수 + func checkAnswer(_ user: [Int], with answer: [Int]) -> (strike: Int, ball: Int, correct: Bool) { + // 힌트 초기화 + var hint: (strike: Int, ball: Int) = (0, 0) + // 힌트 설정(스트라이크, 볼) + for (i, element) in user.enumerated(){ + if answer[i] == element { + hint.strike += 1 + } else if answer.contains(element) { + hint.ball += 1 + } + } + + let correct = hint.strike == 3 ? true : false + + return (strike: hint.strike, ball: hint.ball, correct: correct) + } +} diff --git a/BaseballGame/BaseballGame/Game.swift b/BaseballGame/BaseballGame/Game.swift index 6045caf..cb4a9ff 100644 --- a/BaseballGame/BaseballGame/Game.swift +++ b/BaseballGame/BaseballGame/Game.swift @@ -8,122 +8,91 @@ import Foundation class BaseballGame { - var isExit = false - var isCorrect = false - - var answer: [Int] = [] - var userAnswer: [Int] = [] + // private 캡슐화 + private var isExit = false + private let gameManager = BaseballGameManager() + + // 굳이 변수가 여기 선언될 필요가 있는가. 1회성인데 var gameRecord: [Int] = [] var gameCount = 0 //MARK: 게임 시작 함수 func start() { isExit = false - while !isExit { - if let selected = selectMenu() { - // 입력 번호에 따른 함수 실행 - switch selected { - case .play: - play() - case .record: - record() - case .exit: - isExit = true - } + while !isExit, let selected = gameManager.selectMenu() { + // 입력 번호에 따른 함수 실행 + switch selected { + case .play: + play() + case .record: + record() + case .exit: + isExit = true } } exit() } - - // 메뉴 선택 함수 - func selectMenu() -> Menu? { - print(GameMessage.welcome) - let condition = ["1", "2", "3"] - var menu = inputWithNoSpace() - - // 입력문 유효성 검사 - while !condition.contains(menu) { - print(GameMessage.invalidInput, GameMessage.selectMenuExample) - menu = inputWithNoSpace() // 입력값 공백 제거 함수 확장에 구현 - } - return Menu(rawValue: menu) - } - //MARK: 게임 플레이 함수 func play() { print(GameMessage.startGame) + gameRecord.append(0) // 게임 기록 생성 - setAnswer() // 정답 생성 + let answer = setAnswer() // 정답 생성 + var isCorrect = false + debugPrint("정답: \(answer)") // 정답을 맞힐 때까지 반복 while !isCorrect { - getUserAnswer() - checkAnswer() + let userAnswer = getUserAnswer() + let result = gameManager.checkAnswer(userAnswer, with: answer) + isCorrect = result.correct + print(printResult(result)) gameRecord[gameCount] += 1 // 시도 횟수 증가 } gameCount += 1 // 게임 횟수 증가 } // 정답 생성 함수 - func setAnswer() { - // 초기화 - isCorrect = false - answer = [] - - var num = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // 정답 숫자 후보 - num.shuffle() // 숫자 배열 섞기 - answer = num[0] != 0 ? Array(num[0...2]) : Array(num[1...3]) // 첫 번째 숫자가 0일 경우 예외 처리 + func setAnswer() -> [Int] { +// var num = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // 정답 숫자 후보 +// num.shuffle() // 숫자 배열 섞기 +// let answer = num[0] != 0 ? Array(num[0...2]) : Array(num[1...3]) // 첫 번째 숫자가 0일 경우 예외 처리 - debugPrint("정답: \(answer)") + return Array((0...9).shuffled() // 숫자 섞기 + .trimmingPrefix(while: { $0 == 0 }) // while 조건에 맞는 첫 글자 삭제 + .prefix(3)) + } // 유저 정답 입력 함수 - func getUserAnswer() { + func getUserAnswer() -> [Int] { print(GameMessage.userAnswerExample) - // 초기화 - userAnswer = [] - var isValid = false - - while !isValid { + while true { // 유저 입력 let input = readLine() ?? "" // 유저 입력 배열 - userAnswer = input.compactMap{ Int(String($0)) } + let userAnswer = input.compactMap{ Int(String($0)) } - if userAnswer.count != 3 { + if userAnswer.count != 3 { // 유저 입력이 3자리 숫자가 아닐 경우 print(GameMessage.invalidInput, GameMessage.userAnswerExample) - } else if Set(userAnswer).count != 3 { + } else if Set(userAnswer).count != 3 { // 유저 입력에 중복 숫자가 있을 경우 print(GameMessage.duplicateInput, GameMessage.userAnswerExample) } else { - isValid = true + return userAnswer } } } - // 정답 & 유저 입력 비교 함수 - func checkAnswer() { - // 힌트 초기화 - var hint: (strike: Int, ball: Int) = (0, 0) - // 힌트 설정(스트라이크, 볼) - for (i, element) in userAnswer.enumerated(){ - if answer[i] == element { - hint.strike += 1 - } else if answer.contains(element) { - hint.ball += 1 - } - } - - // 힌트에 따른 분기 처리 - if hint.strike == 3 { - isCorrect = true - print(GameMessage.correct) - } else if hint.strike == 0 && hint.ball == 0 { - print(GameMessage.nothing) + func printResult(_ result: (strike: Int, ball: Int, correct: Bool)) -> String { + if result.correct { + return GameMessage.correct + } else if result.strike == 0 && result.ball == 0 { + return GameMessage.nothing } else { - print(GameMessage.getHint(for: hint.strike, hint.ball)) + return GameMessage.getHint(for: result.strike, result.ball) } } @@ -149,16 +118,17 @@ class BaseballGame { // 게임 기록 초기화 gameRecord = [] gameCount = 0 - + print(GameMessage.endGame) } } //MARK: 부가 기능 구현부 + extension BaseballGame { // 입력값 공백 제거 - func inputWithNoSpace() -> String { + @inlinable func inputWithNoSpace() -> String { // @Inlinable 하면 인라이닝해서 치환되는데.. 요거 별로 안쓰이는데 필요한가? var input = readLine() ?? "" input = input.trimmingCharacters(in: .whitespacesAndNewlines) return input diff --git a/BaseballGame/BaseballGame/Helper.swift b/BaseballGame/BaseballGame/Helper.swift index 9b1cd7d..65585e5 100644 --- a/BaseballGame/BaseballGame/Helper.swift +++ b/BaseballGame/BaseballGame/Helper.swift @@ -7,35 +7,6 @@ import Foundation -enum GameMessage { - static let welcome = """ - 환영합니다! 원하시는 번호를 입력해주세요. (예: 1) - 1. 게임 시작하기 2. 게임 기록 보기 3. 종료하기 - """ - static let startGame = "\n❮ 게임을 시작합니다. ❯" - static let endGame = "❮ 숫자 야구 게임을 종료합니다. ❯" - - static let recordTitle = "❮게임 기록 보기❯\n" - static let noRecord = "플레이한 게임 기록이 없습니다!\n" - - static let selectMenuExample = "실행을 원하는 메뉴의 숫자를 입력해주세요. (예: 1)" - static let userAnswerExample = "서로 다른 3자리 숫자를 입력해주세요. (예: 123)" - - static let invalidInput = "⚠️ 유효하지 않은 입력입니다." - static let duplicateInput = "⚠️ 중복 숫자 입력입니다." - - static let correct = "🎉 정답입니다! 🎉\n" - static let nothing = "❌ Nothing\n" - - static func getHint(for strike: Int, _ ball: Int) -> String { - return "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" - } - - static func getRecord(for game: Int, attempt: Int) -> String { - return "\(game + 1)번째 게임: 시도 횟수 - \(attempt)" - } -} - enum Menu: String { case play = "1", record = "2", exit = "3" } diff --git a/BaseballGame/BaseballGame/Model/GameMessage.swift b/BaseballGame/BaseballGame/Model/GameMessage.swift new file mode 100644 index 0000000..50c9dbd --- /dev/null +++ b/BaseballGame/BaseballGame/Model/GameMessage.swift @@ -0,0 +1,37 @@ +// +// GameMessage.swift +// BaseballGame +// +// Created by 변예린 on 1/15/26. +// + +import Foundation + +enum GameMessage { + static let welcome = """ + 환영합니다! 원하시는 번호를 입력해주세요. (예: 1) + 1. 게임 시작하기 2. 게임 기록 보기 3. 종료하기 + """ + static let startGame = "\n❮ 게임을 시작합니다. ❯" + static let endGame = "❮ 숫자 야구 게임을 종료합니다. ❯" + + static let recordTitle = "❮게임 기록 보기❯\n" + static let noRecord = "플레이한 게임 기록이 없습니다!\n" + + static let selectMenuExample = "실행을 원하는 메뉴의 숫자를 입력해주세요. (예: 1)" + static let userAnswerExample = "서로 다른 3자리 숫자를 입력해주세요. (예: 123)" + + static let invalidInput = "⚠️ 유효하지 않은 입력입니다." + static let duplicateInput = "⚠️ 중복 숫자 입력입니다." + + static let correct = "🎉 정답입니다! 🎉\n" + static let nothing = "❌ Nothing\n" + + static func getHint(for strike: Int, _ ball: Int) -> String { + return "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" + } + + static func getRecord(for game: Int, attempt: Int) -> String { + return "\(game + 1)번째 게임: 시도 횟수 - \(attempt)" + } +} From 5f84ca0c36c2b5fd1d7c709fbd3ac1f75f075174 Mon Sep 17 00:00:00 2001 From: Bambu Date: Fri, 16 Jan 2026 22:45:43 +0900 Subject: [PATCH 03/10] =?UTF-8?q?Refactor:=20=EA=B0=9D=EC=B2=B4=ED=99=94?= =?UTF-8?q?=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/BaseballGameManager.swift | 45 ------ .../BaseballGame/Controller/GameManager.swift | 36 +++++ .../Controller/InputManager.swift | 33 +++++ .../Controller/RecordManager.swift | 40 ++++++ BaseballGame/BaseballGame/Game.swift | 136 ------------------ BaseballGame/BaseballGame/Helper.swift | 12 -- .../BaseballGame/Model/CheckResult.swift | 14 ++ .../BaseballGame/Model/GameMessage.swift | 37 ----- BaseballGame/BaseballGame/Model/Record.swift | 13 ++ .../BaseballGame/View/MessagePrinter.swift | 57 ++++++++ BaseballGame/BaseballGame/main.swift | 8 +- README.md | 3 + 12 files changed, 203 insertions(+), 231 deletions(-) delete mode 100644 BaseballGame/BaseballGame/Controller/BaseballGameManager.swift create mode 100644 BaseballGame/BaseballGame/Controller/GameManager.swift create mode 100644 BaseballGame/BaseballGame/Controller/InputManager.swift create mode 100644 BaseballGame/BaseballGame/Controller/RecordManager.swift delete mode 100644 BaseballGame/BaseballGame/Game.swift delete mode 100644 BaseballGame/BaseballGame/Helper.swift create mode 100644 BaseballGame/BaseballGame/Model/CheckResult.swift delete mode 100644 BaseballGame/BaseballGame/Model/GameMessage.swift create mode 100644 BaseballGame/BaseballGame/Model/Record.swift create mode 100644 BaseballGame/BaseballGame/View/MessagePrinter.swift diff --git a/BaseballGame/BaseballGame/Controller/BaseballGameManager.swift b/BaseballGame/BaseballGame/Controller/BaseballGameManager.swift deleted file mode 100644 index 2366807..0000000 --- a/BaseballGame/BaseballGame/Controller/BaseballGameManager.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// BaseballGameManager.swift -// BaseballGame -// -// Created by 변예린 on 1/15/26. -// - -import Foundation - -struct BaseballGameManager { - //TODO: 환영 인사 / 메뉴 문자열 구분 필요 - /// 게임 시작 화면에서 메뉴를 선택하는 함수입니다. - func selectMenu() -> Menu? { - while true { - print(GameMessage.welcome) // 환영 인사 - let input = (readLine() ?? "").trimmingCharacters(in: .whitespacesAndNewlines) // 유저 입력값 - - if Menu(rawValue: input) != nil { - return Menu(rawValue: input) // 유효한 입력값일 경우 Menu 타입 반환 - } else { - print(GameMessage.invalidInput, GameMessage.selectMenuExample) // 에러 메세지 출력 - } - } - return nil - } - - //TODO: 반환값 타입 정하기 - // 정답 & 유저 입력 비교 함수 - func checkAnswer(_ user: [Int], with answer: [Int]) -> (strike: Int, ball: Int, correct: Bool) { - // 힌트 초기화 - var hint: (strike: Int, ball: Int) = (0, 0) - // 힌트 설정(스트라이크, 볼) - for (i, element) in user.enumerated(){ - if answer[i] == element { - hint.strike += 1 - } else if answer.contains(element) { - hint.ball += 1 - } - } - - let correct = hint.strike == 3 ? true : false - - return (strike: hint.strike, ball: hint.ball, correct: correct) - } -} diff --git a/BaseballGame/BaseballGame/Controller/GameManager.swift b/BaseballGame/BaseballGame/Controller/GameManager.swift new file mode 100644 index 0000000..cd4f1f3 --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/GameManager.swift @@ -0,0 +1,36 @@ +// +// BaseballGameManager.swift +// BaseballGame +// +// Created by 변예린 on 1/15/26. +// + +import Foundation + +class GameManager { + // 게임 정답 생성 함수 + func setAnswer() -> [Int] { + return Array((0...9).shuffled() // 숫자 섞기 + .trimmingPrefix(while: { $0 == 0 }) // while 조건에 맞는 첫 글자 삭제 + .prefix(3)) + } + + // 정답 & 유저 입력 비교 함수 + func check(_ user: [Int], with answer: [Int]) -> CheckResult { + var strike = 0 + var ball = 0 + + // 힌트 설정(스트라이크, 볼) + for (i, element) in user.enumerated(){ + if answer[i] == element { + strike += 1 // 숫자의 자리와 요소가 동일할 경우 + } else if answer.contains(element) { + ball += 1 // 숫자의 요소가 동일할 경우 + } + } + + let isCorrect = strike == 3 ? true : false // 정답 여부 + + return CheckResult(strike: strike, ball: ball, correct: isCorrect) + } +} diff --git a/BaseballGame/BaseballGame/Controller/InputManager.swift b/BaseballGame/BaseballGame/Controller/InputManager.swift new file mode 100644 index 0000000..0e45b9b --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/InputManager.swift @@ -0,0 +1,33 @@ +// +// InputDesk.swift +// BaseballGame +// +// Created by 변예린 on 1/16/26. +// + +import Foundation + +class InputManager { + // 유저로부터 실행할 메뉴를 입력받는 함수 + func inputMenu() -> Menu? { + let input = (readLine() ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + return Menu(rawValue: input) + } + + // 유저로부터 정답을 입력받는 함수 + func inputUserAnswer() -> [Int] { + // 유저 입력값 + let input = (readLine() ?? "").compactMap{ Int(String($0)) } + return input + } + + func verify(_ input: [Int]) -> InputResult { + if input.count != 3 { + return .invalid(for: .answer) // 유저 입력이 3자리 숫자가 아닐 경우 + } else if Set(input).count != 3 { + return .duplicate // 유저 입력에 중복 숫자가 있을 경우 + } else { + return .valid // 정상 입력일 경우 + } + } +} diff --git a/BaseballGame/BaseballGame/Controller/RecordManager.swift b/BaseballGame/BaseballGame/Controller/RecordManager.swift new file mode 100644 index 0000000..9e416f4 --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/RecordManager.swift @@ -0,0 +1,40 @@ +// +// RecordManager.swift +// BaseballGame +// +// Created by 변예린 on 1/16/26. +// + +import Foundation + +class RecordManager { + static let shared = RecordManager() + private var record = Record() + + private init() {} + + // 게임 플레이 횟수 증가 + func addRound() { + // - round 기본값은 0, round는 attempts 배열의 인덱스로 사용됨: attempts[round] + // - 기본값이 attempts 배열보다 앞서있으므로 배열이 비어있을 때는 round를 증가시키지 않음 + // --> round를 증가시킬 경우, 시도 횟수는 attempts[0]에 저장되지만 round == 1이므로 attempts[round]로 조회 불가능 + record.round = record.attempts.isEmpty ? record.round : +1 + record.attempts.append(0) // record.attempts 배열 확장 + } + + // 게임 시도 횟수 증가 + func addAttempt() { + record.attempts[record.round] += 1 + } + + // 기록 초기화 + func resetRecord() { + record.round = 0 + record.attempts = [] + } + + // 기록 전달 + func fetchRecord() -> Record { + return record + } +} diff --git a/BaseballGame/BaseballGame/Game.swift b/BaseballGame/BaseballGame/Game.swift deleted file mode 100644 index cb4a9ff..0000000 --- a/BaseballGame/BaseballGame/Game.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// Game.swift -// BaseballGame -// -// Created by 변예린 on 1/13/26. -// - -import Foundation - -class BaseballGame { - // private 캡슐화 - private var isExit = false - - private let gameManager = BaseballGameManager() - - // 굳이 변수가 여기 선언될 필요가 있는가. 1회성인데 - var gameRecord: [Int] = [] - var gameCount = 0 - - //MARK: 게임 시작 함수 - func start() { - isExit = false - while !isExit, let selected = gameManager.selectMenu() { - // 입력 번호에 따른 함수 실행 - switch selected { - case .play: - play() - case .record: - record() - case .exit: - isExit = true - } - } - exit() - } - - //MARK: 게임 플레이 함수 - func play() { - print(GameMessage.startGame) - - gameRecord.append(0) // 게임 기록 생성 - let answer = setAnswer() // 정답 생성 - var isCorrect = false - debugPrint("정답: \(answer)") - - // 정답을 맞힐 때까지 반복 - while !isCorrect { - let userAnswer = getUserAnswer() - let result = gameManager.checkAnswer(userAnswer, with: answer) - isCorrect = result.correct - print(printResult(result)) - gameRecord[gameCount] += 1 // 시도 횟수 증가 - } - gameCount += 1 // 게임 횟수 증가 - } - - // 정답 생성 함수 - func setAnswer() -> [Int] { -// var num = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // 정답 숫자 후보 -// num.shuffle() // 숫자 배열 섞기 -// let answer = num[0] != 0 ? Array(num[0...2]) : Array(num[1...3]) // 첫 번째 숫자가 0일 경우 예외 처리 - - return Array((0...9).shuffled() // 숫자 섞기 - .trimmingPrefix(while: { $0 == 0 }) // while 조건에 맞는 첫 글자 삭제 - .prefix(3)) - - } - - // 유저 정답 입력 함수 - func getUserAnswer() -> [Int] { - print(GameMessage.userAnswerExample) - - while true { - // 유저 입력 - let input = readLine() ?? "" - // 유저 입력 배열 - let userAnswer = input.compactMap{ Int(String($0)) } - - if userAnswer.count != 3 { // 유저 입력이 3자리 숫자가 아닐 경우 - print(GameMessage.invalidInput, GameMessage.userAnswerExample) - } else if Set(userAnswer).count != 3 { // 유저 입력에 중복 숫자가 있을 경우 - print(GameMessage.duplicateInput, GameMessage.userAnswerExample) - } else { - return userAnswer - } - } - } - - func printResult(_ result: (strike: Int, ball: Int, correct: Bool)) -> String { - if result.correct { - return GameMessage.correct - } else if result.strike == 0 && result.ball == 0 { - return GameMessage.nothing - } else { - return GameMessage.getHint(for: result.strike, result.ball) - } - } - - //MARK: 게임 기록 조회 함수 - func record() { - print(GameMessage.recordTitle) - - // 게임 기록이 없을 경우 - guard !gameRecord.isEmpty else { - print(GameMessage.noRecord) - return - } - - // 게임 기록 출력 - for i in 0.. String { // @Inlinable 하면 인라이닝해서 치환되는데.. 요거 별로 안쓰이는데 필요한가? - var input = readLine() ?? "" - input = input.trimmingCharacters(in: .whitespacesAndNewlines) - return input - } -} diff --git a/BaseballGame/BaseballGame/Helper.swift b/BaseballGame/BaseballGame/Helper.swift deleted file mode 100644 index 65585e5..0000000 --- a/BaseballGame/BaseballGame/Helper.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Error.swift -// BaseballGame -// -// Created by 변예린 on 1/13/26. -// - -import Foundation - -enum Menu: String { - case play = "1", record = "2", exit = "3" -} diff --git a/BaseballGame/BaseballGame/Model/CheckResult.swift b/BaseballGame/BaseballGame/Model/CheckResult.swift new file mode 100644 index 0000000..8a2902b --- /dev/null +++ b/BaseballGame/BaseballGame/Model/CheckResult.swift @@ -0,0 +1,14 @@ +// +// CheckResult.swift +// BaseballGame +// +// Created by 변예린 on 1/16/26. +// + +import Foundation + +struct CheckResult { + let strike: Int + let ball: Int + let correct: Bool +} diff --git a/BaseballGame/BaseballGame/Model/GameMessage.swift b/BaseballGame/BaseballGame/Model/GameMessage.swift deleted file mode 100644 index 50c9dbd..0000000 --- a/BaseballGame/BaseballGame/Model/GameMessage.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// GameMessage.swift -// BaseballGame -// -// Created by 변예린 on 1/15/26. -// - -import Foundation - -enum GameMessage { - static let welcome = """ - 환영합니다! 원하시는 번호를 입력해주세요. (예: 1) - 1. 게임 시작하기 2. 게임 기록 보기 3. 종료하기 - """ - static let startGame = "\n❮ 게임을 시작합니다. ❯" - static let endGame = "❮ 숫자 야구 게임을 종료합니다. ❯" - - static let recordTitle = "❮게임 기록 보기❯\n" - static let noRecord = "플레이한 게임 기록이 없습니다!\n" - - static let selectMenuExample = "실행을 원하는 메뉴의 숫자를 입력해주세요. (예: 1)" - static let userAnswerExample = "서로 다른 3자리 숫자를 입력해주세요. (예: 123)" - - static let invalidInput = "⚠️ 유효하지 않은 입력입니다." - static let duplicateInput = "⚠️ 중복 숫자 입력입니다." - - static let correct = "🎉 정답입니다! 🎉\n" - static let nothing = "❌ Nothing\n" - - static func getHint(for strike: Int, _ ball: Int) -> String { - return "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" - } - - static func getRecord(for game: Int, attempt: Int) -> String { - return "\(game + 1)번째 게임: 시도 횟수 - \(attempt)" - } -} diff --git a/BaseballGame/BaseballGame/Model/Record.swift b/BaseballGame/BaseballGame/Model/Record.swift new file mode 100644 index 0000000..785c229 --- /dev/null +++ b/BaseballGame/BaseballGame/Model/Record.swift @@ -0,0 +1,13 @@ +// +// Record.swift +// BaseballGame +// +// Created by 변예린 on 1/16/26. +// + +import Foundation + +class Record { + var round = 0 + var attempts: [Int] = [] +} diff --git a/BaseballGame/BaseballGame/View/MessagePrinter.swift b/BaseballGame/BaseballGame/View/MessagePrinter.swift new file mode 100644 index 0000000..3502dbc --- /dev/null +++ b/BaseballGame/BaseballGame/View/MessagePrinter.swift @@ -0,0 +1,57 @@ +// +// MessagePrinter.swift +// BaseballGame +// +// Created by 변예린 on 1/16/26. +// + +import Foundation + +class MessagePrinter { + func welcome() { + print(GameMessage.welcome) + print(GameMessage.menu) + } + + func startGame() { + print(GameMessage.startGame) + } + + func endGame() { + print(GameMessage.endGame) + } + + func showRecordTitle() { + print(GameMessage.recordTitle) + } + + func noRecord() { + print(GameMessage.noRecord) + } + + func error(_ error: InputResult) { + switch error { + case .invalid(.menu): + print(GameMessage.invalidInput, "\n") + print(GameMessage.selectMenuExample) + print(GameMessage.menu) + case .invalid(.answer): + print(GameMessage.invalidInput, "\n") + print(GameMessage.userAnswerExample) + case .duplicate: + print(GameMessage.duplicateInput, "\n") + print(GameMessage.userAnswerExample) + case .valid: break + } + } + + func result(_ result: CheckResult) { + if result.correct { + print(GameMessage.correct) + } else if result.strike == 0 && result.ball == 0 { + print(GameMessage.nothing) + } else { + print(GameMessage.getHint(for: result.strike, result.ball)) + } + } +} diff --git a/BaseballGame/BaseballGame/main.swift b/BaseballGame/BaseballGame/main.swift index 5656f75..4de270c 100644 --- a/BaseballGame/BaseballGame/main.swift +++ b/BaseballGame/BaseballGame/main.swift @@ -4,5 +4,11 @@ // // Created by 변예린 on 1/13/26. // -let game = BaseballGame() +let messagePrinter = MessagePrinter() +let inputManager = InputManager() +let recordManager = RecordManager.shared +let gameManager = GameManager() + +let game = BaseballGame(messagePrinter: messagePrinter, recordManager: recordManager, inputManager: inputManager, gameManager: gameManager) + game.start() diff --git a/README.md b/README.md index 93b031f..e59ea35 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ 숫자 야구 게임은 컴퓨터가 생성한 중복 없는 숫자를 맞히는 콘솔 기반 게임입니다. 사용자는 숫자를 입력하고 **스트라이크 / 볼 / 아웃** 결과를 통해 정답을 추론합니다. + + + ## 1. BaseballGame 타입 선택 ### 1) Struct vs. Class 처음에는 구조체로 구현했으나, 내부 값을 계속해서 변경해나가야하는데 구조체는 그때마다 객체 자체를 다시 써야한다는 번거로움이 있습니다. (함수 앞에도 mutating 키워드를 계속 붙여줘야합니다.) 따라서 참조 타입인 클래스로 변경하였습니다. From 982e6b034d10f3003b8559d36465e28f09bcd934 Mon Sep 17 00:00:00 2001 From: Bambu Date: Fri, 16 Jan 2026 22:46:44 +0900 Subject: [PATCH 04/10] no message --- .../BaseballGame/Controller/Game.swift | 110 ++++++++++++++++++ .../BaseballGame/Helper/GameMessage.swift | 36 ++++++ BaseballGame/BaseballGame/Helper/Helper.swift | 23 ++++ 3 files changed, 169 insertions(+) create mode 100644 BaseballGame/BaseballGame/Controller/Game.swift create mode 100644 BaseballGame/BaseballGame/Helper/GameMessage.swift create mode 100644 BaseballGame/BaseballGame/Helper/Helper.swift diff --git a/BaseballGame/BaseballGame/Controller/Game.swift b/BaseballGame/BaseballGame/Controller/Game.swift new file mode 100644 index 0000000..b2e5567 --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/Game.swift @@ -0,0 +1,110 @@ +// +// Game.swift +// BaseballGame +// +// Created by 변예린 on 1/13/26. +// + +import Foundation + +class BaseballGame { + private let messagePrinter: MessagePrinter + private let recordManager: RecordManager + private let inputManager: InputManager + private let gameManager: GameManager + + init(messagePrinter: MessagePrinter, recordManager: RecordManager, inputManager: InputManager, gameManager: GameManager) { + self.messagePrinter = messagePrinter + self.recordManager = recordManager + self.inputManager = inputManager + self.gameManager = gameManager + } + + // 게임 시작 함수 + func start() { + var isExit = false + + while !isExit { + messagePrinter.welcome() + let selected = selectMenu() // 메뉴 선택 + + // 선택 메뉴에 따른 함수 실행 + switch selected { + case .play: + messagePrinter.startGame() + play() + case .record: + messagePrinter.showRecordTitle() + showRecord() + case .exit: + recordManager.resetRecord() + messagePrinter.endGame() + isExit = true + } + } + } + + // 메뉴 선택 함수 + private func selectMenu() -> Menu { + while true { + // 유저 입력값 + let input = inputManager.inputMenu() + + if let input = input { + return input // 유효한 입력값일 경우 Menu 타입 반환 + } else { + messagePrinter.error(.invalid(for: .menu)) // 에러 메세지 출력 + } + } + } + + // 게임 플레이 함수 + private func play() { + recordManager.addRound() // 게임 기록 생성 + let answer = gameManager.setAnswer() // 정답 생성 + + debugPrint(answer) // 디버깅용 정답 출력 + + while true { // 정답을 맞힐 때까지 반복 + let userAnswer = getUserAnswer() + + let result = gameManager.check(userAnswer, with: answer) + messagePrinter.result(result) + + recordManager.addAttempt() + if result.correct { break } + } + } + + // 유저 정답 생성 함수 + private func getUserAnswer() -> [Int] { + while true { + let input = inputManager.inputUserAnswer() // 유저 입력값 + let verification = inputManager.verify(input) // 유효성 검사 + + if verification == .valid { + return input // 정상 입력일 경우 + } else if verification == .duplicate { + messagePrinter.error(.duplicate) // 유저 입력에 중복 숫자가 있을 경우 + } else if verification == .invalid(for: .answer) { + messagePrinter.error(.invalid(for: .answer)) // 유저 입력이 3자리 숫자가 아닐 경우 + } + } + } + + // 기록 조회 함수 + private func showRecord() { + let record = recordManager.fetchRecord() + + // 기록이 없는 경우 + if record.attempts.isEmpty { + messagePrinter.noRecord() + } else { + // 게임 기록 출력 + for i in record.attempts.indices { + print(GameMessage.getRecord(for: i, attempt: record.attempts[i])) + } + print("\n", terminator: "") + } + } +} diff --git a/BaseballGame/BaseballGame/Helper/GameMessage.swift b/BaseballGame/BaseballGame/Helper/GameMessage.swift new file mode 100644 index 0000000..d8b8842 --- /dev/null +++ b/BaseballGame/BaseballGame/Helper/GameMessage.swift @@ -0,0 +1,36 @@ +// +// GameMessage.swift +// BaseballGame +// +// Created by 변예린 on 1/15/26. +// + +import Foundation + +enum GameMessage { + static let welcome = "환영합니다! 원하시는 메뉴를 입력해주세요. (예: 1)" + static let menu = "1. 게임 시작하기 2. 게임 기록 보기 3. 종료하기" + + static let startGame = "\n❮ 게임을 시작합니다. ❯" + static let endGame = "❮ 숫자 야구 게임을 종료합니다. ❯" + + static let recordTitle = "❮게임 기록 보기❯\n" + static let noRecord = "플레이한 게임 기록이 없습니다!\n" + + static let selectMenuExample = "실행을 원하는 메뉴의 숫자를 입력해주세요. (예: 1)" + static let userAnswerExample = "서로 다른 3자리 숫자를 입력해주세요. (예: 123)" + + static let invalidInput = "⚠️ 유효하지 않은 입력입니다." + static let duplicateInput = "⚠️ 중복 숫자 입력입니다." + + static let correct = "🎉 정답입니다! 🎉\n" + static let nothing = "❌ Nothing\n" + + static func getHint(for strike: Int, _ ball: Int) -> String { + return "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" + } + + static func getRecord(for game: Int, attempt: Int) -> String { + return "\(game + 1)번째 게임: 시도 횟수 - \(attempt)" + } +} diff --git a/BaseballGame/BaseballGame/Helper/Helper.swift b/BaseballGame/BaseballGame/Helper/Helper.swift new file mode 100644 index 0000000..4721efa --- /dev/null +++ b/BaseballGame/BaseballGame/Helper/Helper.swift @@ -0,0 +1,23 @@ +// +// Error.swift +// BaseballGame +// +// Created by 변예린 on 1/13/26. +// + +import Foundation + +enum Menu: String { + case play = "1", record = "2", exit = "3" +} + +enum InputResult: Equatable { + case invalid(for: Item) + case duplicate + case valid +} + +enum Item { + case menu + case answer +} From 0e0afe5a31acfbc1e94e2d3817cf6cea3ecd4f45 Mon Sep 17 00:00:00 2001 From: Bambu Date: Mon, 19 Jan 2026 10:38:15 +0900 Subject: [PATCH 05/10] =?UTF-8?q?Refactor:=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=B0=8F=20Readme=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{GameManager.swift => GameComputer.swift} | 3 +- .../{Game.swift => GameController.swift} | 63 +++--- .../Controller/InputManager.swift | 26 ++- .../Controller/RecordManager.swift | 1 + .../BaseballGame/Helper/GameMessage.swift | 1 + BaseballGame/BaseballGame/Helper/Helper.swift | 5 + BaseballGame/BaseballGame/Model/Record.swift | 1 + .../BaseballGame/View/MessagePrinter.swift | 9 +- BaseballGame/BaseballGame/main.swift | 4 +- README.md | 206 ++++++++++-------- 10 files changed, 184 insertions(+), 135 deletions(-) rename BaseballGame/BaseballGame/Controller/{GameManager.swift => GameComputer.swift} (92%) rename BaseballGame/BaseballGame/Controller/{Game.swift => GameController.swift} (55%) diff --git a/BaseballGame/BaseballGame/Controller/GameManager.swift b/BaseballGame/BaseballGame/Controller/GameComputer.swift similarity index 92% rename from BaseballGame/BaseballGame/Controller/GameManager.swift rename to BaseballGame/BaseballGame/Controller/GameComputer.swift index cd4f1f3..3d954c9 100644 --- a/BaseballGame/BaseballGame/Controller/GameManager.swift +++ b/BaseballGame/BaseballGame/Controller/GameComputer.swift @@ -7,7 +7,8 @@ import Foundation -class GameManager { +// 게임과 관련된 연산을 담당하는 클래스입니다. +class GameComputer { // 게임 정답 생성 함수 func setAnswer() -> [Int] { return Array((0...9).shuffled() // 숫자 섞기 diff --git a/BaseballGame/BaseballGame/Controller/Game.swift b/BaseballGame/BaseballGame/Controller/GameController.swift similarity index 55% rename from BaseballGame/BaseballGame/Controller/Game.swift rename to BaseballGame/BaseballGame/Controller/GameController.swift index b2e5567..063369b 100644 --- a/BaseballGame/BaseballGame/Controller/Game.swift +++ b/BaseballGame/BaseballGame/Controller/GameController.swift @@ -7,17 +7,20 @@ import Foundation -class BaseballGame { +/* 게임의 시스템을 관리하는 클래스입니다. + 각 기능을 담당하는 클래스에게 명령을 내려 핵심 기능을 수행합니다. */ + +class GameController { private let messagePrinter: MessagePrinter private let recordManager: RecordManager private let inputManager: InputManager - private let gameManager: GameManager + private let gameComputer: GameComputer - init(messagePrinter: MessagePrinter, recordManager: RecordManager, inputManager: InputManager, gameManager: GameManager) { + init(messagePrinter: MessagePrinter, recordManager: RecordManager, inputManager: InputManager, gameComputer: GameComputer) { self.messagePrinter = messagePrinter self.recordManager = recordManager self.inputManager = inputManager - self.gameManager = gameManager + self.gameComputer = gameComputer } // 게임 시작 함수 @@ -30,13 +33,13 @@ class BaseballGame { // 선택 메뉴에 따른 함수 실행 switch selected { - case .play: + case .play: // 게임 시작 messagePrinter.startGame() play() - case .record: + case .record: // 기록 조회 messagePrinter.showRecordTitle() showRecord() - case .exit: + case .exit: // 게임 종료 recordManager.resetRecord() messagePrinter.endGame() isExit = true @@ -47,13 +50,13 @@ class BaseballGame { // 메뉴 선택 함수 private func selectMenu() -> Menu { while true { - // 유저 입력값 - let input = inputManager.inputMenu() - - if let input = input { - return input // 유효한 입력값일 경우 Menu 타입 반환 - } else { - messagePrinter.error(.invalid(for: .menu)) // 에러 메세지 출력 + do { + let input = try inputManager.inputMenu() // 유저 입력값 + return input // 유효할 경우 + } catch InputError.invalid(for: .menu) { + messagePrinter.error(.invalid(for: .menu)) // 오류 메세지 출력 + } catch { + messagePrinter.unknownError() // 알 수 없는 오류 } } } @@ -61,40 +64,42 @@ class BaseballGame { // 게임 플레이 함수 private func play() { recordManager.addRound() // 게임 기록 생성 - let answer = gameManager.setAnswer() // 정답 생성 + let answer = gameComputer.setAnswer() // 정답 생성 debugPrint(answer) // 디버깅용 정답 출력 while true { // 정답을 맞힐 때까지 반복 - let userAnswer = getUserAnswer() + let userAnswer = getUserAnswer() // 유저 정답 생성 - let result = gameManager.check(userAnswer, with: answer) - messagePrinter.result(result) + // 정답 확인 + let result = gameComputer.check(userAnswer, with: answer) + messagePrinter.result(result) // 결과 출력 + // 기록 변경 recordManager.addAttempt() - if result.correct { break } + if result.correct { break } // 정답 시 게임 종료 } } // 유저 정답 생성 함수 private func getUserAnswer() -> [Int] { while true { - let input = inputManager.inputUserAnswer() // 유저 입력값 - let verification = inputManager.verify(input) // 유효성 검사 - - if verification == .valid { - return input // 정상 입력일 경우 - } else if verification == .duplicate { - messagePrinter.error(.duplicate) // 유저 입력에 중복 숫자가 있을 경우 - } else if verification == .invalid(for: .answer) { - messagePrinter.error(.invalid(for: .answer)) // 유저 입력이 3자리 숫자가 아닐 경우 + do { + let input = try inputManager.inputUserAnswer() + return input + } catch InputError.duplicate { + messagePrinter.error(.duplicate) + } catch InputError.invalid(for: .answer) { + messagePrinter.error(.invalid(for: .answer)) + } catch { + messagePrinter.unknownError() } } } // 기록 조회 함수 private func showRecord() { - let record = recordManager.fetchRecord() + let record = recordManager.fetchRecord() // 기록 불러오기 // 기록이 없는 경우 if record.attempts.isEmpty { diff --git a/BaseballGame/BaseballGame/Controller/InputManager.swift b/BaseballGame/BaseballGame/Controller/InputManager.swift index 0e45b9b..e0e2ad7 100644 --- a/BaseballGame/BaseballGame/Controller/InputManager.swift +++ b/BaseballGame/BaseballGame/Controller/InputManager.swift @@ -7,27 +7,31 @@ import Foundation +// 유저에게 값을 입력받고 검증하는 클래스입니다. class InputManager { // 유저로부터 실행할 메뉴를 입력받는 함수 - func inputMenu() -> Menu? { + func inputMenu() throws -> Menu { let input = (readLine() ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - return Menu(rawValue: input) + + if let result = Menu(rawValue: input) { + return result + } else { + throw InputError.invalid(for: .menu) + } + } // 유저로부터 정답을 입력받는 함수 - func inputUserAnswer() -> [Int] { + func inputUserAnswer() throws -> [Int] { // 유저 입력값 let input = (readLine() ?? "").compactMap{ Int(String($0)) } - return input - } - - func verify(_ input: [Int]) -> InputResult { + if input.count != 3 { - return .invalid(for: .answer) // 유저 입력이 3자리 숫자가 아닐 경우 + throw InputError.invalid(for: .answer) // 유저 입력이 3자리 숫자가 아닐 경우 } else if Set(input).count != 3 { - return .duplicate // 유저 입력에 중복 숫자가 있을 경우 + throw InputError.duplicate // 유저 입력에 중복 숫자가 있을 경우 } else { - return .valid // 정상 입력일 경우 + return input //정상 입력일 경우 } - } + } } diff --git a/BaseballGame/BaseballGame/Controller/RecordManager.swift b/BaseballGame/BaseballGame/Controller/RecordManager.swift index 9e416f4..575f6ff 100644 --- a/BaseballGame/BaseballGame/Controller/RecordManager.swift +++ b/BaseballGame/BaseballGame/Controller/RecordManager.swift @@ -7,6 +7,7 @@ import Foundation +// 게임 기록을 관리하는 클래스입니다. class RecordManager { static let shared = RecordManager() private var record = Record() diff --git a/BaseballGame/BaseballGame/Helper/GameMessage.swift b/BaseballGame/BaseballGame/Helper/GameMessage.swift index d8b8842..9294a52 100644 --- a/BaseballGame/BaseballGame/Helper/GameMessage.swift +++ b/BaseballGame/BaseballGame/Helper/GameMessage.swift @@ -22,6 +22,7 @@ enum GameMessage { static let invalidInput = "⚠️ 유효하지 않은 입력입니다." static let duplicateInput = "⚠️ 중복 숫자 입력입니다." + static let unknownError = "⚠️ 알 수 없는 오류입니다." static let correct = "🎉 정답입니다! 🎉\n" static let nothing = "❌ Nothing\n" diff --git a/BaseballGame/BaseballGame/Helper/Helper.swift b/BaseballGame/BaseballGame/Helper/Helper.swift index 4721efa..06865a0 100644 --- a/BaseballGame/BaseballGame/Helper/Helper.swift +++ b/BaseballGame/BaseballGame/Helper/Helper.swift @@ -21,3 +21,8 @@ enum Item { case menu case answer } + +enum InputError: Error { + case duplicate + case invalid(for: Item) +} diff --git a/BaseballGame/BaseballGame/Model/Record.swift b/BaseballGame/BaseballGame/Model/Record.swift index 785c229..1cfba07 100644 --- a/BaseballGame/BaseballGame/Model/Record.swift +++ b/BaseballGame/BaseballGame/Model/Record.swift @@ -7,6 +7,7 @@ import Foundation +// 게임 기록 클래스입니다. 변동 가능성이 크기 때문에 구조체가 아닌 클래스로 구현하였습니다. class Record { var round = 0 var attempts: [Int] = [] diff --git a/BaseballGame/BaseballGame/View/MessagePrinter.swift b/BaseballGame/BaseballGame/View/MessagePrinter.swift index 3502dbc..51170bd 100644 --- a/BaseballGame/BaseballGame/View/MessagePrinter.swift +++ b/BaseballGame/BaseballGame/View/MessagePrinter.swift @@ -7,6 +7,7 @@ import Foundation +// GameMessage를 출력하는 클래스입니다. class MessagePrinter { func welcome() { print(GameMessage.welcome) @@ -15,6 +16,7 @@ class MessagePrinter { func startGame() { print(GameMessage.startGame) + print(GameMessage.userAnswerExample) } func endGame() { @@ -29,7 +31,7 @@ class MessagePrinter { print(GameMessage.noRecord) } - func error(_ error: InputResult) { + func error(_ error: InputError) { switch error { case .invalid(.menu): print(GameMessage.invalidInput, "\n") @@ -41,10 +43,13 @@ class MessagePrinter { case .duplicate: print(GameMessage.duplicateInput, "\n") print(GameMessage.userAnswerExample) - case .valid: break } } + func unknownError() { + print(GameMessage.unknownError) + } + func result(_ result: CheckResult) { if result.correct { print(GameMessage.correct) diff --git a/BaseballGame/BaseballGame/main.swift b/BaseballGame/BaseballGame/main.swift index 4de270c..9a15981 100644 --- a/BaseballGame/BaseballGame/main.swift +++ b/BaseballGame/BaseballGame/main.swift @@ -7,8 +7,8 @@ let messagePrinter = MessagePrinter() let inputManager = InputManager() let recordManager = RecordManager.shared -let gameManager = GameManager() +let gameComputer = GameComputer() -let game = BaseballGame(messagePrinter: messagePrinter, recordManager: recordManager, inputManager: inputManager, gameManager: gameManager) +let game = GameController(messagePrinter: messagePrinter, recordManager: recordManager, inputManager: inputManager, gameComputer: gameComputer) game.start() diff --git a/README.md b/README.md index e59ea35..b529520 100644 --- a/README.md +++ b/README.md @@ -3,113 +3,149 @@ 숫자 야구 게임은 컴퓨터가 생성한 중복 없는 숫자를 맞히는 콘솔 기반 게임입니다. 사용자는 숫자를 입력하고 **스트라이크 / 볼 / 아웃** 결과를 통해 정답을 추론합니다. +---------- +## 1. 프로젝트 소개 +### 1) 프로젝트 구조 +```swift +├── Controller +│   ├── GameController.swift // 게임 시스템 관리 +│   ├── GameComputer.swift // 게임 연산 담당 +│   ├── InputManager.swift // 유저 입력 및 검증 담당 +│   └── RecordManager.swift // 기록 관리 담당 +├── Helper +│   ├── GameMessage.swift // 게임 메세지 문자열 +│   └── Helper.swift // 부가적으로 필요한 열거형 +├── main.swift +├── Model +│   ├── CheckResult.swift // 정답 확인 결과 모델 +│   └── Record.swift // 게임 기록 모델 +└── View + └── MessagePrinter.swift // 게임 메세지 출력 담당 +``` +객체화를 진행하며 기능에 따라 객체들을 분리하다보니 MVC 패턴으로 구분지어봐도 될 것 같아 시도해보았습니다. -## 1. BaseballGame 타입 선택 -### 1) Struct vs. Class -처음에는 구조체로 구현했으나, 내부 값을 계속해서 변경해나가야하는데 구조체는 그때마다 객체 자체를 다시 써야한다는 번거로움이 있습니다. (함수 앞에도 mutating 키워드를 계속 붙여줘야합니다.) 따라서 참조 타입인 클래스로 변경하였습니다. +- **Model** : 게임 내에서 데이터로 사용될 객체 +- **View** : 게임 UI 관련 객체 +- **Controller** : 게임 시스템 관련 동작 객체 +- **Helper** : 위 3가지 분류에 해당되지 않는 부가 객체 -### 2) hint 튜플 -`hint`는 스트라이크와 볼로만 구분됩니다. -2개보다 더 많은 요소가 추가될 필요가 없고, 값에 이름을 붙여 직관적인 코드를 작성할 수 있다는 장점때문에 튜플을 선택하였습니다. +위 기준으로 분리하였습니다. -### 3) Menu 열거형 -`selectMenu()` 함수의 `menu`는 `input`에서 공백만 제거한 문자열입니다. 처음에는 `start`와 `selectMenu` 함수를 분리하지 않았기에 문자열 그대로 분기처리하여 메뉴별 함수를 호출하였습니다. +각 Controller와 View 객체는 만약 이 프로젝트가 커진다고 가정했을 때, 재사용성을 고려하면 프로젝트의 여러 곳에서 동일한 하나의 객체를 가리키게 하는 편이 낫지않을까 생각하여 클래스로 구현하였습니다. -(함수의 분리에 관한 내용은 2.2)에서 자세히 서술합니다.) +### 2) 설계 시 고려했던 부분 +**GameController 클래스** +Image -```swift -switch menu { -case "1": - play() -case "2": - record() -case "3": - return -default: - return -} -``` +게임의 전체적인 시스템을 관리하는 클래스입니다. + +각 기능을 담당하는 클래스에게 명령을 내려 핵심 기능을 수행하게 합니다. + +내부에서 다른 Controller 클래스들을 참조하기도 하고, 실제 개발 환경이었다면 `GameController`이라는 부모 클래스를 상속받아서 `BaseballGameController`이라는 클래스가 생성될 수도 있지 않을까 생각하여 클래스로 구현하였습니다. + +- `selectMenu()`, `play()`, `showRecord()`, +: 메뉴를 선택하고 각 메뉴의 기능을 동작하는 함수입니다. + + GameController는 각 클래스들을 모아서 동작을 명령하고 게임을 주도하는 관리자같은 존재입니다. + + 따라서 게임 진행과 관련있는 기능들은 GameController 내에서 구현되어야 GameController가 게임을 주도할 수 있다고 생각했습니다. + + 그때문에 메인 메뉴의 기능을 동작하는 함수를 GameController에서 선언하고, 함수 내부에서 기능을 구현하기 위해 각 클래스로의 동작을 명령합니다. + + 즉, 해당 함수들은 클래스로의 일종의 동작 명령 모음인 셈입니다. + +- `getUserAnswer() -> [Int]` +: 처음에는 GameComputer내에 선언되었던 함수입니다. + +하지만 GameComputer에서 함수가 동작하기 위해서는 GameComputer내에서 inputManager와 messagePrinter가 동작해야합니다. -그러나 두 함수의 분리로 `menu`를 `selectMenu` 내부가 아닌 외부 함수 `start`에서도 사용하게 되었습니다. +게다가 GameComputer는 연산만을 담당하는 클래스인데, 해당 동작은 연산이 아닌 유효한 유저 입력값을 반환하는 것이 목적이라 클래스의 기능과는 맞지 않다고 생각했습니다. -`start`에서도 `menu`를 문자열 그대로 사용하여 분기처리할 경우 원하는 케이스 외에도 default를 정의해야 합니다. 게다가 열거형은 문자열보다 메모리를 덜 차지한다는 장점이 있으므로 여러 방면에서 열거형으로 사용하는 것이 적합하다고 생각하였습니다. +GameController는 관리자로써 각 Controller 클래스를 연결해주는 중재자(매개체)로서의 기능도 하고있습니다. + +`getUserAnswer` 함수는 InputManager로부터 입력값을 받아 다른 클래스로 전달하기 위해 사용됩니다. + +따라서 이는 GameController가 담당할 기능이라 생각하여 해당 클래스 내에 구현하게 되었습니다. + +**GameComputer 클래스** + Image + + 게임 관련 연산을 담당하는 클래스입니다. + +- `setAnswer() -> [Int]` + +처음에는 for문을 활용해 정답을 생성하였습니다. ```swift - func start() { - while !isExit { - ... - if let selected = selectedMenu { - // 입력 번호에 따른 함수 실행 - switch selected { - case .play: - play() - case .record: - record() - case .exit: - isExit = true - } +func setAnswer() { + let answer = [] + + for _ in 0...2 { + // 정답 첫 번째 숫자일 경우 + if answer.isEmpty { + let num = Int.random(in: 1...9) + answer.append(num) + } else { + var num = Int.random(in: 0...9) + // 정답에 포함되어있다면 num 재생성 + while answer.contains(num) { + num = Int.random(in: 0...9) } + answer.append(num) } - exit() } +} +``` + +이후 튜터님께서 `shuffle()` 메서드를 활용해볼 것을 제안하셔서 적용해보았습니다. + +``` +func setAnswer() { + ... + + var num = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] // 정답 숫자 후보 + num.shuffle() // 숫자 배열 섞기 + answer = num[0] != 0 ? Array(num[0...2]) : Array(num[1...3]) // 첫 번째 숫자가 0일 경우 예외 처리 +} ``` -### 4) gameRecord 배열 -`gameRecord`는 게임 기록을 출력하기 위해 저장하는 게임 기록 배열입니다. -처음에는 `[Int: Int]` 형태의 딕셔너리로 구현하려했으나, key 값으로 쓰일 `gameCount`는 단순 숫자이므로 key로써의 의의가 떨어진다고 여겼습니다. +`num` 배열을 생성하여 코드를 작성했는데, 생성하지 않고도 메서드를 활용해 바로 정답 배열을 만들 수 있다는 피드백을 들어 다시 수정하였습니다. -특정 번째 게임의 기록을 랜덤하게 불러오는 것이 아니기 때문에 순서대로 값이 저장되는 배열이 게임 기록을 저장하기에 적합한 타입이라 생각했습니다. +```func setAnswer() -> [Int] { + return Array((0...9).shuffled() + .trimmingPrefix(while: { $0 == 0 }).prefix(3) +} +``` -이후 특정 게임 기록이 필요하더라도, key값이 단순 숫자인 이상 인덱스로 값을 불러오는 배열과 딕셔너리가 기능 면에서 차이가 없을거라 생각했습니다. +`trimmingPrefix(while:)`을 활용하여 `(0...9).shuffled()`의 첫 요소가 0일 경우 0을 잘라내고 첫 3개의 요소를 바로 반환하도록 하였습니다. -오히려 배열이 key의 hash 값을 찾을 필요가 없기 때문에 성능면에서도 우위가 있으리라 판단하여 배열을 사용했습니다. + +**RecordManager 클래스** +Image -## 2. 함수의 분리 -### 1) `getUserAnswer()`와 `checkAnswer()` -두 함수를 하나로 합쳐 구현할 수도 있었지만 (실제로 그렇게 구현하기도 했었지만) 유저의 정답을 얻는 것과 정답을 확인하는 것은 기능이 다르다고 생각하여 분리하였습니다. +게임 기록을 관리하는 클래스로, 싱글톤 패턴을 사용해보았습니다. -`play()` 함수에서 함수들을 호출하여 사용하므로 게임의 흐름이 잘 보일 수 있도록 기능을 구분하여 구현하는 것이 적합하다고 생각했습니다. +게임 기록은 야구 게임 내에서 유일한 기록입니다. 따라서 해당 기록을 변경시키는 존재 또한 유일해야한다고 판단하여 싱글톤 패턴을 적용해보았습니다. -### 2) `start()`와 `selectMenu()` -앞선 1.3)에서 언급했듯 처음에는 `selectMenu` 함수 없이 `start` 함수에서 `menu` 문자열을 그대로 분기처리하여 switch문에서 각 메뉴에 맞는 함수를 바로 실행하도록 하였습니다. +**Record 클래스** -그러나 문제에서 각 메뉴가 실행된 후 '종료하기'를 제외하고는 실행 이후 **다시 메뉴 선택 화면이 나오도록 요구**하고 있습니다. +게임 기록 그 자체를 의미하는 클래스입니다. -이를 충족하기 위해서는 '메뉴 선택'과 '게임 프로그램 시작' 기능을 분리해야한다고 생각했습니다. (정확히는, '메뉴 선택' 기능이 모듈화 되어야한다고 생각했습니다.) +RecordManager와 같은 이유로, 생성된 **하나의** 게임 기록이 지속해서 변화해야한다고 생각했으므로 게임 기록 모델인 `Record` 또한 클래스로 구현해보았습니다. -```swift - func start() { - while !isExit { - ... - - guard let selected = selectMenu() else { - print("유효하지 않은 입력입니다!") - return - } - - ... +클래스 내부의 `attempts`는 한 게임 라운드의 시도 횟수를 저장하는 배열입니다. - } - } -``` +처음에는 `round`와 `attempts`를 `[Int: Int]` 형태의 딕셔너리로 구현하려했으나, key 값으로 쓰일 `round`는 단순 숫자이므로 key로써의 의의가 떨어진다고 판단했습니다. + +특정 라운드의 기록을 랜덤하게 불러오는 것이 아니기 때문에 순서대로 값이 저장되는 배열이 게임 기록을 저장하기에 적합한 타입이라 생각했습니다. -따라서 `selectMenu` 함수를 분리하고 해당 함수를 통해 `menu`를 반환받아 `start`에서 분기처리하여 실행하는 방식으로 수정하였습니다. - -> **✏️ `selected` 처리 방식 수정** -> -> guard문 → if문으로 수정하였습니다. -> ```swift -> if let selected = selectMenu() { -> switch selected { -> ... -> } ->} ->``` -> `selectMenu` 함수 내부에서 이미 유효성 검사를 하고 값이 반환되기 때문에 예외 처리를 다시 하지 않고 `nil`값이면 함수를 종료하도록 하였습니다. - -## 3. 트러블 슈팅 +이후 특정 게임 기록이 필요하더라도, key값이 단순 숫자인 이상 인덱스로 값을 불러오는 배열과 딕셔너리가 기능 면에서 차이가 없을거라 생각했습니다. + +오히려 배열이 key의 hash 값을 찾을 필요가 없기 때문에 성능면에서도 우위가 있으리라 판단하여 배열을 사용했습니다. + +## 2. 트러블 슈팅 ### 1) 필수 구현 1번 #### ⚠️ 문제: 중복 숫자가 포함되는 정답 생성 ```swift @@ -172,16 +208,6 @@ do { default catch문을 작성해줌으로써 해결하였습니다. -> **🧐 에러 타입이 지금 필요한가?** -> ->앞서 유효하지 않은 값에 대한 오류를 여러번 다뤄야할 것 같아 에러 타입을 정의했다고 언급했습니다. -> ->하지만 구현해나가다보니 생각보다 오류 케이스가 많지 않고(현재로써는 1개뿐), 그에 비해 default catch문을 포함한 do-catch문을 사용하기 위해 더 많은 코드가 작성된다고 여겨집니다. -> ->따라서 나중을 대비해 에러 타입 자체는 남겨두고 함수는 throws를 하지 않도록 변경하였습니다. -> ->예외 처리는 대부분 guard문을 통해 오류 내용을 출력하는 것으로 수정하였습니다. - ### 3) 추가 구현 #### ⚠️ 문제: 가변 문자열의 열거형 케이스 구현 어려움 From c7058858df6d92b5417d4cd695d1e94c6ac09e12 Mon Sep 17 00:00:00 2001 From: Bambu Date: Mon, 19 Jan 2026 10:44:59 +0900 Subject: [PATCH 06/10] =?UTF-8?q?Chore:=20InputResult=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BaseballGame/BaseballGame/Helper/Helper.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/BaseballGame/BaseballGame/Helper/Helper.swift b/BaseballGame/BaseballGame/Helper/Helper.swift index 06865a0..f8f4135 100644 --- a/BaseballGame/BaseballGame/Helper/Helper.swift +++ b/BaseballGame/BaseballGame/Helper/Helper.swift @@ -11,12 +11,6 @@ enum Menu: String { case play = "1", record = "2", exit = "3" } -enum InputResult: Equatable { - case invalid(for: Item) - case duplicate - case valid -} - enum Item { case menu case answer From 32ffb744afcd2391afad11bdba739d1b833606a5 Mon Sep 17 00:00:00 2001 From: Bambu Date: Mon, 19 Jan 2026 10:55:32 +0900 Subject: [PATCH 07/10] Update README.md --- README.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b529520..7b62e52 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,12 @@ ### 2) 설계 시 고려했던 부분 **GameController 클래스** -Image +

+ +

게임의 전체적인 시스템을 관리하는 클래스입니다. @@ -70,8 +75,13 @@ GameController는 관리자로써 각 Controller 클래스를 연결해주는 따라서 이는 GameController가 담당할 기능이라 생각하여 해당 클래스 내에 구현하게 되었습니다. **GameComputer 클래스** - Image - +

+ +

+ 게임 관련 연산을 담당하는 클래스입니다. - `setAnswer() -> [Int]` @@ -123,7 +133,12 @@ func setAnswer() { **RecordManager 클래스** -Image +

+ +

게임 기록을 관리하는 클래스로, 싱글톤 패턴을 사용해보았습니다. From 9f2ae40b22cd0e76482a65edfa55d110cd17bb57 Mon Sep 17 00:00:00 2001 From: Bambu Date: Mon, 19 Jan 2026 11:08:08 +0900 Subject: [PATCH 08/10] Update README.md --- README.md | 79 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 7b62e52..4d4fd65 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ ---------- -## 1. 프로젝트 소개 -### 1) 프로젝트 구조 +# 1. 프로젝트 소개 +## 1) 프로젝트 구조 ```swift ├── Controller │   ├── GameController.swift // 게임 시스템 관리 @@ -35,8 +35,8 @@ 각 Controller와 View 객체는 만약 이 프로젝트가 커진다고 가정했을 때, 재사용성을 고려하면 프로젝트의 여러 곳에서 동일한 하나의 객체를 가리키게 하는 편이 낫지않을까 생각하여 클래스로 구현하였습니다. -### 2) 설계 시 고려했던 부분 -**GameController 클래스** +## 2) 설계 시 고려했던 부분 +### 1️⃣ GameController 클래스


+✏️ **`selectMenu()`, `play()`, `showRecord()`** + +메뉴를 선택하고 각 메뉴의 기능을 동작하는 함수입니다. GameController는 각 클래스들을 모아서 동작을 명령하고 게임을 주도하는 관리자같은 존재입니다. @@ -60,9 +62,11 @@ 그때문에 메인 메뉴의 기능을 동작하는 함수를 GameController에서 선언하고, 함수 내부에서 기능을 구현하기 위해 각 클래스로의 동작을 명령합니다. 즉, 해당 함수들은 클래스로의 일종의 동작 명령 모음인 셈입니다. + +

+✏️ **`getUserAnswer() -> [Int]`** -- `getUserAnswer() -> [Int]` -: 처음에는 GameComputer내에 선언되었던 함수입니다. +처음에는 GameComputer내에 선언되었던 함수입니다. 하지만 GameComputer에서 함수가 동작하기 위해서는 GameComputer내에서 inputManager와 messagePrinter가 동작해야합니다. @@ -73,8 +77,9 @@ GameController는 관리자로써 각 Controller 클래스를 연결해주는 `getUserAnswer` 함수는 InputManager로부터 입력값을 받아 다른 클래스로 전달하기 위해 사용됩니다. 따라서 이는 GameController가 담당할 기능이라 생각하여 해당 클래스 내에 구현하게 되었습니다. - -**GameComputer 클래스** + +

+### 2️⃣ GameComputer 클래스

[Int]` +

+✏️ **`setAnswer() -> [Int]`** 처음에는 for문을 활용해 정답을 생성하였습니다. @@ -131,11 +137,11 @@ func setAnswer() { `trimmingPrefix(while:)`을 활용하여 `(0...9).shuffled()`의 첫 요소가 0일 경우 0을 잘라내고 첫 3개의 요소를 바로 반환하도록 하였습니다. - -**RecordManager 클래스** +

+### 3️⃣ RecordManager 클래스

@@ -144,7 +150,8 @@ func setAnswer() { 게임 기록은 야구 게임 내에서 유일한 기록입니다. 따라서 해당 기록을 변경시키는 존재 또한 유일해야한다고 판단하여 싱글톤 패턴을 적용해보았습니다. -**Record 클래스** +

+### 4️⃣ Record 클래스 게임 기록 그 자체를 의미하는 클래스입니다. @@ -159,10 +166,13 @@ RecordManager와 같은 이유로, 생성된 **하나의** 게임 기록이 지 이후 특정 게임 기록이 필요하더라도, key값이 단순 숫자인 이상 인덱스로 값을 불러오는 배열과 딕셔너리가 기능 면에서 차이가 없을거라 생각했습니다. 오히려 배열이 key의 hash 값을 찾을 필요가 없기 때문에 성능면에서도 우위가 있으리라 판단하여 배열을 사용했습니다. +

+ +---------- -## 2. 트러블 슈팅 -### 1) 필수 구현 1번 -#### ⚠️ 문제: 중복 숫자가 포함되는 정답 생성 +# 2. 트러블 슈팅 +## 1) 필수 구현 1번 +### ⚠️ 문제: 중복 숫자가 포함되는 정답 생성 ```swift func setAnswer() { for _ in 0...2 { @@ -172,12 +182,14 @@ func setAnswer() { ``` → 중복 여부를 확인하지 않고 랜덤 숫자를 생성하고 있음 -#### ❗️ 원인: 중복 생성 방지 코드의 부재 +

+### ❗️ 원인: 중복 생성 방지 코드의 부재 문제 요구사항을 정독하지 않아 중복 숫자 생성을 막는 코드를 작성하지 못했습니다. 중복 숫자가 있는 경우, 힌트를 통해 유저가 올바른 정답을 떠올리기 어렵기 때문에 힌트의 의미가 사라집니다. -#### ✅ 해결: 조건문 추가 +

+### ✅ 해결: 조건문 추가 ```swift while answer.contains(num) { num = Int.random(in: 0...9) @@ -187,8 +199,9 @@ answer.append(num) 조건문을 추가하여 중복 숫자의 생성을 막아주었습니다. -### 2) 추가 구현 -#### ⚠️ 문제: 에러 핸들링 오류 +

+## 2) 추가 구현 - 1 +### ⚠️ 문제: 에러 핸들링 오류 유효하지 않은 값에 대한 오류를 여러번 다뤄야할 것 같아 에러 타입을 정의하였습니다. ```swift @@ -203,13 +216,15 @@ enum GameError { Image -#### ❗️ 원인: default 에러 핸들링 코드의 부재 +

+### ❗️ 원인: default 에러 핸들링 코드의 부재 찾아보니 스위프트는 `throws`가 포함된 함수라면 '에러'를 던진다는 사실만 알지, 정확히 어떠한 에러를 던질지는 알 수 없다고 합니다. 따라서 제가 던졌던 `GameError`뿐만 아니라 (가능성은 매우 낮으나) 던져질 수 있는 정의되지 않은 다른 에러에 대해서도 처리를 해주어야 한다고 합니다. -#### ✅ 해결: default 핸들링 코드 작성 +

+### ✅ 해결: default 핸들링 코드 작성 ```swift do { @@ -223,8 +238,9 @@ do { default catch문을 작성해줌으로써 해결하였습니다. -### 3) 추가 구현 -#### ⚠️ 문제: 가변 문자열의 열거형 케이스 구현 어려움 +

+## 3) 추가 구현 - 2 +### ⚠️ 문제: 가변 문자열의 열거형 케이스 구현 어려움 기존 직접 입력하여 출력하던 문자열들을 열거형 타입 하나로 묶어 열거형을 호출해 출력하는 방식으로 리팩토링을 시도했습니다. ```swift @@ -248,7 +264,8 @@ if hint.strike == 3 { } ``` -#### ❗️ 원인: 열거형의 문자열 원시값 정의 +

+### ❗️ 원인: 열거형의 문자열 원시값 정의 `GameMessage` 케이스 별로 다른 연관값을 주어 해결하고자 했지만, 그 경우에는 외부에서 열거형 객체를 생성해주어야한다는 단점이 있었습니다. @@ -284,7 +301,8 @@ print(m.toString()) // "n 스트라이크 n 볼 입니다" 출력 위처럼 구현하면 돌아가기는 하지만... 좀더 간결한 방법은 없을까 싶어 튜터님께 조언을 구했습니다. -#### ✅ 해결 방법1: 확장과 프로토콜 활용하기 +

+### ✅ 해결 방법1: 확장과 프로토콜 활용하기 ```swift enum GameMessage { case welcome @@ -313,7 +331,8 @@ extensionSystemMessage: CustomStringConvertible { print("\(GameMessage.welcome)") // "환영합니다!" 출력 ``` -#### ✅ 해결 방법2: 타입 변수 활용하기 +

+### ✅ 해결 방법2: 타입 변수 활용하기 ```swift enum GameMessage { static var welcome = "환영합니다!" From 9695ffe017652f143982104be2e0a2a827d7632d Mon Sep 17 00:00:00 2001 From: Bambu Date: Mon, 19 Jan 2026 19:50:24 +0900 Subject: [PATCH 09/10] =?UTF-8?q?Chore:=20=EA=B3=BC=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EC=84=A4=20=EC=B0=B8=EA=B3=A0=20=EC=9D=BC=EB=B6=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GameController: start() 종료 부분 변경 - InputManager: getUserAnswer() 기존 코드 오류 가능성으로 유효성 검사부 조건 추가 - GameMessage: 스트라이크 or 볼 0일 시 분기 처리 추가 --- BaseballGame/BaseballGame/Controller/GameController.swift | 6 ++---- BaseballGame/BaseballGame/Controller/InputManager.swift | 7 ++++--- BaseballGame/BaseballGame/Helper/GameMessage.swift | 8 +++++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/BaseballGame/BaseballGame/Controller/GameController.swift b/BaseballGame/BaseballGame/Controller/GameController.swift index 063369b..c065965 100644 --- a/BaseballGame/BaseballGame/Controller/GameController.swift +++ b/BaseballGame/BaseballGame/Controller/GameController.swift @@ -25,9 +25,7 @@ class GameController { // 게임 시작 함수 func start() { - var isExit = false - - while !isExit { + while true { messagePrinter.welcome() let selected = selectMenu() // 메뉴 선택 @@ -42,7 +40,7 @@ class GameController { case .exit: // 게임 종료 recordManager.resetRecord() messagePrinter.endGame() - isExit = true + exit(0) } } } diff --git a/BaseballGame/BaseballGame/Controller/InputManager.swift b/BaseballGame/BaseballGame/Controller/InputManager.swift index e0e2ad7..18bfe59 100644 --- a/BaseballGame/BaseballGame/Controller/InputManager.swift +++ b/BaseballGame/BaseballGame/Controller/InputManager.swift @@ -24,10 +24,11 @@ class InputManager { // 유저로부터 정답을 입력받는 함수 func inputUserAnswer() throws -> [Int] { // 유저 입력값 - let input = (readLine() ?? "").compactMap{ Int(String($0)) } + let stringInput = (readLine() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).map{ String($0) } // [String] 변환 + let input = stringInput.compactMap{ Int($0) } // [Int] 변환 - if input.count != 3 { - throw InputError.invalid(for: .answer) // 유저 입력이 3자리 숫자가 아닐 경우 + if input.count != 3 || input.count != stringInput.count { + throw InputError.invalid(for: .answer) // 유저 입력이 3자리 숫자가 아닐 경우 혹은 유저 입력에 문자가 같이 입력되었을 경우 } else if Set(input).count != 3 { throw InputError.duplicate // 유저 입력에 중복 숫자가 있을 경우 } else { diff --git a/BaseballGame/BaseballGame/Helper/GameMessage.swift b/BaseballGame/BaseballGame/Helper/GameMessage.swift index 9294a52..b033305 100644 --- a/BaseballGame/BaseballGame/Helper/GameMessage.swift +++ b/BaseballGame/BaseballGame/Helper/GameMessage.swift @@ -28,7 +28,13 @@ enum GameMessage { static let nothing = "❌ Nothing\n" static func getHint(for strike: Int, _ ball: Int) -> String { - return "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" + if strike == 0 { + return "⚾️ \(ball) 볼 입니다!\n" + } else if ball == 0 { + return "🎯 \(strike) 스트라이크 입니다!\n" + } else { + return "🎯 \(strike) 스트라이크 ⚾️ \(ball) 볼 입니다!\n" + } } static func getRecord(for game: Int, attempt: Int) -> String { From cfa55290e3f6e0d17b9903c35b2b6caf4f8c01bf Mon Sep 17 00:00:00 2001 From: Bambu Date: Tue, 20 Jan 2026 21:00:03 +0900 Subject: [PATCH 10/10] =?UTF-8?q?Chore:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=95=20-=20RecordManager=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RecordManager addRound() 증가가 안되던 버그 수정 GameController 게임 종료 조건 변경 --- BaseballGame/BaseballGame/Controller/GameController.swift | 6 ++++-- BaseballGame/BaseballGame/Controller/RecordManager.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/BaseballGame/BaseballGame/Controller/GameController.swift b/BaseballGame/BaseballGame/Controller/GameController.swift index c065965..063369b 100644 --- a/BaseballGame/BaseballGame/Controller/GameController.swift +++ b/BaseballGame/BaseballGame/Controller/GameController.swift @@ -25,7 +25,9 @@ class GameController { // 게임 시작 함수 func start() { - while true { + var isExit = false + + while !isExit { messagePrinter.welcome() let selected = selectMenu() // 메뉴 선택 @@ -40,7 +42,7 @@ class GameController { case .exit: // 게임 종료 recordManager.resetRecord() messagePrinter.endGame() - exit(0) + isExit = true } } } diff --git a/BaseballGame/BaseballGame/Controller/RecordManager.swift b/BaseballGame/BaseballGame/Controller/RecordManager.swift index 575f6ff..fb4d26e 100644 --- a/BaseballGame/BaseballGame/Controller/RecordManager.swift +++ b/BaseballGame/BaseballGame/Controller/RecordManager.swift @@ -19,7 +19,7 @@ class RecordManager { // - round 기본값은 0, round는 attempts 배열의 인덱스로 사용됨: attempts[round] // - 기본값이 attempts 배열보다 앞서있으므로 배열이 비어있을 때는 round를 증가시키지 않음 // --> round를 증가시킬 경우, 시도 횟수는 attempts[0]에 저장되지만 round == 1이므로 attempts[round]로 조회 불가능 - record.round = record.attempts.isEmpty ? record.round : +1 + record.round = record.attempts.isEmpty ? record.round : record.round + 1 record.attempts.append(0) // record.attempts 배열 확장 }