diff --git a/BaseballGame/BaseballGame/Controller/GameComputer.swift b/BaseballGame/BaseballGame/Controller/GameComputer.swift new file mode 100644 index 0000000..3d954c9 --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/GameComputer.swift @@ -0,0 +1,37 @@ +// +// BaseballGameManager.swift +// BaseballGame +// +// Created by 변예린 on 1/15/26. +// + +import Foundation + +// 게임과 관련된 연산을 담당하는 클래스입니다. +class GameComputer { + // 게임 정답 생성 함수 + 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/GameController.swift b/BaseballGame/BaseballGame/Controller/GameController.swift new file mode 100644 index 0000000..063369b --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/GameController.swift @@ -0,0 +1,115 @@ +// +// Game.swift +// BaseballGame +// +// Created by 변예린 on 1/13/26. +// + +import Foundation + +/* 게임의 시스템을 관리하는 클래스입니다. + 각 기능을 담당하는 클래스에게 명령을 내려 핵심 기능을 수행합니다. */ + +class GameController { + private let messagePrinter: MessagePrinter + private let recordManager: RecordManager + private let inputManager: InputManager + private let gameComputer: GameComputer + + init(messagePrinter: MessagePrinter, recordManager: RecordManager, inputManager: InputManager, gameComputer: GameComputer) { + self.messagePrinter = messagePrinter + self.recordManager = recordManager + self.inputManager = inputManager + self.gameComputer = gameComputer + } + + // 게임 시작 함수 + 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 { + do { + let input = try inputManager.inputMenu() // 유저 입력값 + return input // 유효할 경우 + } catch InputError.invalid(for: .menu) { + messagePrinter.error(.invalid(for: .menu)) // 오류 메세지 출력 + } catch { + messagePrinter.unknownError() // 알 수 없는 오류 + } + } + } + + // 게임 플레이 함수 + private func play() { + recordManager.addRound() // 게임 기록 생성 + let answer = gameComputer.setAnswer() // 정답 생성 + + debugPrint(answer) // 디버깅용 정답 출력 + + while true { // 정답을 맞힐 때까지 반복 + let userAnswer = getUserAnswer() // 유저 정답 생성 + + // 정답 확인 + let result = gameComputer.check(userAnswer, with: answer) + messagePrinter.result(result) // 결과 출력 + + // 기록 변경 + recordManager.addAttempt() + if result.correct { break } // 정답 시 게임 종료 + } + } + + // 유저 정답 생성 함수 + private func getUserAnswer() -> [Int] { + while true { + 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() // 기록 불러오기 + + // 기록이 없는 경우 + 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/Controller/InputManager.swift b/BaseballGame/BaseballGame/Controller/InputManager.swift new file mode 100644 index 0000000..18bfe59 --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/InputManager.swift @@ -0,0 +1,38 @@ +// +// InputDesk.swift +// BaseballGame +// +// Created by 변예린 on 1/16/26. +// + +import Foundation + +// 유저에게 값을 입력받고 검증하는 클래스입니다. +class InputManager { + // 유저로부터 실행할 메뉴를 입력받는 함수 + func inputMenu() throws -> Menu { + let input = (readLine() ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + + if let result = Menu(rawValue: input) { + return result + } else { + throw InputError.invalid(for: .menu) + } + + } + + // 유저로부터 정답을 입력받는 함수 + func inputUserAnswer() throws -> [Int] { + // 유저 입력값 + let stringInput = (readLine() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).map{ String($0) } // [String] 변환 + let input = stringInput.compactMap{ Int($0) } // [Int] 변환 + + if input.count != 3 || input.count != stringInput.count { + throw InputError.invalid(for: .answer) // 유저 입력이 3자리 숫자가 아닐 경우 혹은 유저 입력에 문자가 같이 입력되었을 경우 + } else if Set(input).count != 3 { + throw InputError.duplicate // 유저 입력에 중복 숫자가 있을 경우 + } else { + return input //정상 입력일 경우 + } + } +} diff --git a/BaseballGame/BaseballGame/Controller/RecordManager.swift b/BaseballGame/BaseballGame/Controller/RecordManager.swift new file mode 100644 index 0000000..fb4d26e --- /dev/null +++ b/BaseballGame/BaseballGame/Controller/RecordManager.swift @@ -0,0 +1,41 @@ +// +// 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 : 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 e5f557f..0000000 --- a/BaseballGame/BaseballGame/Game.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// Game.swift -// BaseballGame -// -// Created by 변예린 on 1/13/26. -// - -import Foundation - -class BaseballGame { - var isExit = false - var isCorrect = false - - var answer: [Int] = [] - var userAnswer: [Int] = [] - - 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 - } - } - } - exit() - } - - // 메뉴 선택 함수 - func selectMenu() -> Menu? { - print(GameMessage.welcome) - - let condition = ["1", "2", "3"] - var menu = "" - - // 메뉴 입력 - var input = readLine() ?? "" - // 입력문 공백 삭제 - menu = input.replacingOccurrences(of: " ", with: "") - - // 입력문 유효성 검사 - 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 - } - } - - //MARK: 게임 플레이 함수 - func play() { - print(GameMessage.startGame) - gameRecord.append(0) // 게임 기록 생성 - setAnswer() // 정답 생성 - - // 정답을 맞힐 때까지 반복 - while !isCorrect { - getUserAnswer() - checkAnswer() - gameRecord[gameCount] += 1 // 시도 횟수 증가 - } - gameCount += 1 // 게임 횟수 증가 - } - - // 정답 생성 함수 - func setAnswer() { - // 초기화 - 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) - } - } - debugPrint("정답: \(answer)") - } - - // 유저 정답 입력 함수 - func getUserAnswer() { - print(GameMessage.userAnswerExample) - - // 초기화 - userAnswer = [] - var isValid = false - - while !isValid { - // 유저 입력 - let input = readLine() ?? "" - // 유저 입력 배열 - userAnswer = input.compactMap{ Int(String($0)) } - - if userAnswer.count != 3 { - print(GameMessage.invalidInput, GameMessage.userAnswerExample) - } else if Set(userAnswer).count != 3 { - print(GameMessage.duplicateInput, GameMessage.userAnswerExample) - } else { - isValid = true - } - } - } - - // 정답 & 유저 입력 비교 함수 - func checkAnswer() { - // 힌트 초기화 - var hint: (strike: Int, ball: Int) = (0, 0) - // 힌트 설정(스트라이크, 볼) - userAnswer.enumerated().forEach { - if answer[$0.offset] == $0.element { - hint.strike += 1 - } else if answer.contains($0.element) { - hint.ball += 1 - } - } - - // 힌트에 따른 분기 처리 - print(GameMessage.getHint(for: hint.strike, hint.ball)) - if hint.strike == 3 { isCorrect = true } - } - - //MARK: 게임 기록 조회 함수 - func record() { - print(GameMessage.recordTitle) - - // 게임 기록이 없을 경우 - guard !gameRecord.isEmpty else { - print(GameMessage.noRecord) - return - } - - // 게임 기록 출력 - for i in 0.. String { - return strike == 3 ? "🎉 정답입니다! 🎉\n" - : strike == 0 && ball == 0 ? "❌ Nothing\n" : - "🎯 \(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 { return "\(game + 1)번째 게임: 시도 횟수 - \(attempt)" } } - -enum Menu: String { - case play = "1", record = "2", exit = "3" -} diff --git a/BaseballGame/BaseballGame/Helper/Helper.swift b/BaseballGame/BaseballGame/Helper/Helper.swift new file mode 100644 index 0000000..f8f4135 --- /dev/null +++ b/BaseballGame/BaseballGame/Helper/Helper.swift @@ -0,0 +1,22 @@ +// +// Error.swift +// BaseballGame +// +// Created by 변예린 on 1/13/26. +// + +import Foundation + +enum Menu: String { + case play = "1", record = "2", exit = "3" +} + +enum Item { + case menu + case answer +} + +enum InputError: Error { + case duplicate + case invalid(for: Item) +} 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/Record.swift b/BaseballGame/BaseballGame/Model/Record.swift new file mode 100644 index 0000000..1cfba07 --- /dev/null +++ b/BaseballGame/BaseballGame/Model/Record.swift @@ -0,0 +1,14 @@ +// +// 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..51170bd --- /dev/null +++ b/BaseballGame/BaseballGame/View/MessagePrinter.swift @@ -0,0 +1,62 @@ +// +// MessagePrinter.swift +// BaseballGame +// +// Created by 변예린 on 1/16/26. +// + +import Foundation + +// GameMessage를 출력하는 클래스입니다. +class MessagePrinter { + func welcome() { + print(GameMessage.welcome) + print(GameMessage.menu) + } + + func startGame() { + print(GameMessage.startGame) + print(GameMessage.userAnswerExample) + } + + func endGame() { + print(GameMessage.endGame) + } + + func showRecordTitle() { + print(GameMessage.recordTitle) + } + + func noRecord() { + print(GameMessage.noRecord) + } + + func error(_ error: InputError) { + 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) + } + } + + func unknownError() { + print(GameMessage.unknownError) + } + + 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..9a15981 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 gameComputer = GameComputer() + +let game = GameController(messagePrinter: messagePrinter, recordManager: recordManager, inputManager: inputManager, gameComputer: gameComputer) + game.start() diff --git a/README.md b/README.md index 93b031f..4d4fd65 100644 --- a/README.md +++ b/README.md @@ -3,112 +3,176 @@ 숫자 야구 게임은 컴퓨터가 생성한 중복 없는 숫자를 맞히는 콘솔 기반 게임입니다. 사용자는 숫자를 입력하고 **스트라이크 / 볼 / 아웃** 결과를 통해 정답을 추론합니다. -## 1. BaseballGame 타입 선택 -### 1) Struct vs. Class -처음에는 구조체로 구현했으나, 내부 값을 계속해서 변경해나가야하는데 구조체는 그때마다 객체 자체를 다시 써야한다는 번거로움이 있습니다. (함수 앞에도 mutating 키워드를 계속 붙여줘야합니다.) 따라서 참조 타입인 클래스로 변경하였습니다. +---------- -### 2) hint 튜플 -`hint`는 스트라이크와 볼로만 구분됩니다. -2개보다 더 많은 요소가 추가될 필요가 없고, 값에 이름을 붙여 직관적인 코드를 작성할 수 있다는 장점때문에 튜플을 선택하였습니다. +# 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 // 게임 메세지 출력 담당 +``` -### 3) Menu 열거형 -`selectMenu()` 함수의 `menu`는 `input`에서 공백만 제거한 문자열입니다. 처음에는 `start`와 `selectMenu` 함수를 분리하지 않았기에 문자열 그대로 분기처리하여 메뉴별 함수를 호출하였습니다. +객체화를 진행하며 기능에 따라 객체들을 분리하다보니 MVC 패턴으로 구분지어봐도 될 것 같아 시도해보았습니다. -(함수의 분리에 관한 내용은 2.2)에서 자세히 서술합니다.) +- **Model** : 게임 내에서 데이터로 사용될 객체 +- **View** : 게임 UI 관련 객체 +- **Controller** : 게임 시스템 관련 동작 객체 +- **Helper** : 위 3가지 분류에 해당되지 않는 부가 객체 -```swift -switch menu { -case "1": - play() -case "2": - record() -case "3": - return -default: - return -} -``` +위 기준으로 분리하였습니다. + +각 Controller와 View 객체는 만약 이 프로젝트가 커진다고 가정했을 때, 재사용성을 고려하면 프로젝트의 여러 곳에서 동일한 하나의 객체를 가리키게 하는 편이 낫지않을까 생각하여 클래스로 구현하였습니다. + +## 2) 설계 시 고려했던 부분 +### 1️⃣ GameController 클래스 +

+ +

+ +게임의 전체적인 시스템을 관리하는 클래스입니다. + +각 기능을 담당하는 클래스에게 명령을 내려 핵심 기능을 수행하게 합니다. + +내부에서 다른 Controller 클래스들을 참조하기도 하고, 실제 개발 환경이었다면 `GameController`이라는 부모 클래스를 상속받아서 `BaseballGameController`이라는 클래스가 생성될 수도 있지 않을까 생각하여 클래스로 구현하였습니다. + +

+✏️ **`selectMenu()`, `play()`, `showRecord()`** + +메뉴를 선택하고 각 메뉴의 기능을 동작하는 함수입니다. + + GameController는 각 클래스들을 모아서 동작을 명령하고 게임을 주도하는 관리자같은 존재입니다. + + 따라서 게임 진행과 관련있는 기능들은 GameController 내에서 구현되어야 GameController가 게임을 주도할 수 있다고 생각했습니다. + + 그때문에 메인 메뉴의 기능을 동작하는 함수를 GameController에서 선언하고, 함수 내부에서 기능을 구현하기 위해 각 클래스로의 동작을 명령합니다. + + 즉, 해당 함수들은 클래스로의 일종의 동작 명령 모음인 셈입니다. + +

+✏️ **`getUserAnswer() -> [Int]`** -그러나 두 함수의 분리로 `menu`를 `selectMenu` 내부가 아닌 외부 함수 `start`에서도 사용하게 되었습니다. +처음에는 GameComputer내에 선언되었던 함수입니다. -`start`에서도 `menu`를 문자열 그대로 사용하여 분기처리할 경우 원하는 케이스 외에도 default를 정의해야 합니다. 게다가 열거형은 문자열보다 메모리를 덜 차지한다는 장점이 있으므로 여러 방면에서 열거형으로 사용하는 것이 적합하다고 생각하였습니다. +하지만 GameComputer에서 함수가 동작하기 위해서는 GameComputer내에서 inputManager와 messagePrinter가 동작해야합니다. + +게다가 GameComputer는 연산만을 담당하는 클래스인데, 해당 동작은 연산이 아닌 유효한 유저 입력값을 반환하는 것이 목적이라 클래스의 기능과는 맞지 않다고 생각했습니다. + +GameController는 관리자로써 각 Controller 클래스를 연결해주는 중재자(매개체)로서의 기능도 하고있습니다. + +`getUserAnswer` 함수는 InputManager로부터 입력값을 받아 다른 클래스로 전달하기 위해 사용됩니다. + +따라서 이는 GameController가 담당할 기능이라 생각하여 해당 클래스 내에 구현하게 되었습니다. + +

+### 2️⃣ GameComputer 클래스 +

+ +

+ + 게임 관련 연산을 담당하는 클래스입니다. + +

+✏️ **`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() } +} ``` -### 4) gameRecord 배열 -`gameRecord`는 게임 기록을 출력하기 위해 저장하는 게임 기록 배열입니다. -처음에는 `[Int: Int]` 형태의 딕셔너리로 구현하려했으나, key 값으로 쓰일 `gameCount`는 단순 숫자이므로 key로써의 의의가 떨어진다고 여겼습니다. +이후 튜터님께서 `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일 경우 예외 처리 +} +``` -이후 특정 게임 기록이 필요하더라도, key값이 단순 숫자인 이상 인덱스로 값을 불러오는 배열과 딕셔너리가 기능 면에서 차이가 없을거라 생각했습니다. +`num` 배열을 생성하여 코드를 작성했는데, 생성하지 않고도 메서드를 활용해 바로 정답 배열을 만들 수 있다는 피드백을 들어 다시 수정하였습니다. -오히려 배열이 key의 hash 값을 찾을 필요가 없기 때문에 성능면에서도 우위가 있으리라 판단하여 배열을 사용했습니다. +```func setAnswer() -> [Int] { + return Array((0...9).shuffled() + .trimmingPrefix(while: { $0 == 0 }).prefix(3) +} +``` -## 2. 함수의 분리 -### 1) `getUserAnswer()`와 `checkAnswer()` -두 함수를 하나로 합쳐 구현할 수도 있었지만 (실제로 그렇게 구현하기도 했었지만) 유저의 정답을 얻는 것과 정답을 확인하는 것은 기능이 다르다고 생각하여 분리하였습니다. +`trimmingPrefix(while:)`을 활용하여 `(0...9).shuffled()`의 첫 요소가 0일 경우 0을 잘라내고 첫 3개의 요소를 바로 반환하도록 하였습니다. -`play()` 함수에서 함수들을 호출하여 사용하므로 게임의 흐름이 잘 보일 수 있도록 기능을 구분하여 구현하는 것이 적합하다고 생각했습니다. +

+### 3️⃣ RecordManager 클래스 +

+ +

-### 2) `start()`와 `selectMenu()` -앞선 1.3)에서 언급했듯 처음에는 `selectMenu` 함수 없이 `start` 함수에서 `menu` 문자열을 그대로 분기처리하여 switch문에서 각 메뉴에 맞는 함수를 바로 실행하도록 하였습니다. +게임 기록을 관리하는 클래스로, 싱글톤 패턴을 사용해보았습니다. -그러나 문제에서 각 메뉴가 실행된 후 '종료하기'를 제외하고는 실행 이후 **다시 메뉴 선택 화면이 나오도록 요구**하고 있습니다. +게임 기록은 야구 게임 내에서 유일한 기록입니다. 따라서 해당 기록을 변경시키는 존재 또한 유일해야한다고 판단하여 싱글톤 패턴을 적용해보았습니다. -이를 충족하기 위해서는 '메뉴 선택'과 '게임 프로그램 시작' 기능을 분리해야한다고 생각했습니다. (정확히는, '메뉴 선택' 기능이 모듈화 되어야한다고 생각했습니다.) +

+### 4️⃣ Record 클래스 -```swift - func start() { - while !isExit { - ... - - guard let selected = selectMenu() else { - print("유효하지 않은 입력입니다!") - return - } - - ... +게임 기록 그 자체를 의미하는 클래스입니다. - } - } -``` +RecordManager와 같은 이유로, 생성된 **하나의** 게임 기록이 지속해서 변화해야한다고 생각했으므로 게임 기록 모델인 `Record` 또한 클래스로 구현해보았습니다. + +클래스 내부의 `attempts`는 한 게임 라운드의 시도 횟수를 저장하는 배열입니다. -따라서 `selectMenu` 함수를 분리하고 해당 함수를 통해 `menu`를 반환받아 `start`에서 분기처리하여 실행하는 방식으로 수정하였습니다. - -> **✏️ `selected` 처리 방식 수정** -> -> guard문 → if문으로 수정하였습니다. -> ```swift -> if let selected = selectMenu() { -> switch selected { -> ... -> } ->} ->``` -> `selectMenu` 함수 내부에서 이미 유효성 검사를 하고 값이 반환되기 때문에 예외 처리를 다시 하지 않고 `nil`값이면 함수를 종료하도록 하였습니다. - -## 3. 트러블 슈팅 -### 1) 필수 구현 1번 -#### ⚠️ 문제: 중복 숫자가 포함되는 정답 생성 +처음에는 `round`와 `attempts`를 `[Int: Int]` 형태의 딕셔너리로 구현하려했으나, key 값으로 쓰일 `round`는 단순 숫자이므로 key로써의 의의가 떨어진다고 판단했습니다. + +특정 라운드의 기록을 랜덤하게 불러오는 것이 아니기 때문에 순서대로 값이 저장되는 배열이 게임 기록을 저장하기에 적합한 타입이라 생각했습니다. + +이후 특정 게임 기록이 필요하더라도, key값이 단순 숫자인 이상 인덱스로 값을 불러오는 배열과 딕셔너리가 기능 면에서 차이가 없을거라 생각했습니다. + +오히려 배열이 key의 hash 값을 찾을 필요가 없기 때문에 성능면에서도 우위가 있으리라 판단하여 배열을 사용했습니다. +

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

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

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

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

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

+### ✅ 해결: default 핸들링 코드 작성 ```swift do { @@ -169,18 +238,9 @@ do { default catch문을 작성해줌으로써 해결하였습니다. -> **🧐 에러 타입이 지금 필요한가?** -> ->앞서 유효하지 않은 값에 대한 오류를 여러번 다뤄야할 것 같아 에러 타입을 정의했다고 언급했습니다. -> ->하지만 구현해나가다보니 생각보다 오류 케이스가 많지 않고(현재로써는 1개뿐), 그에 비해 default catch문을 포함한 do-catch문을 사용하기 위해 더 많은 코드가 작성된다고 여겨집니다. -> ->따라서 나중을 대비해 에러 타입 자체는 남겨두고 함수는 throws를 하지 않도록 변경하였습니다. -> ->예외 처리는 대부분 guard문을 통해 오류 내용을 출력하는 것으로 수정하였습니다. - -### 3) 추가 구현 -#### ⚠️ 문제: 가변 문자열의 열거형 케이스 구현 어려움 +

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

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

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

+### ✅ 해결 방법2: 타입 변수 활용하기 ```swift enum GameMessage { static var welcome = "환영합니다!"