Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 161 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,162 @@
# ⚾ 숫자 야구 게임
# ⚾ 숫자 야구 게임

숫자 야구 게임은 컴퓨터가 생성한 중복 없는 숫자를 맞히는 콘솔 기반 게임입니다.
사용자는 숫자를 입력하고 **스트라이크 / 볼 / 아웃** 결과를 통해 정답을 추론합니다.
Swift로 구현한 콘솔 기반 숫자 야구 게임입니다.
게임의 흐름, 규칙, 기록을 각각의 책임으로 나누어 설계하는 것을 목표로 했습니다.

---

## 📌 프로젝트 목적

* Swift 기본 문법 및 제어 흐름 이해
* 클래스 단위 책임 분리 연습
* Set / Array 활용
* 접근 제어자를 통한 캡슐화 경험
* 계산 로직과 출력 로직 분리

---

## 🎮 게임 규칙

* 정답은 **중복되지 않는 3자리 숫자**
* **백의 자리는 1~9**, 나머지는 **0~9**
* 입력값에 따라 다음 힌트를 제공

* **Strike**: 숫자와 위치가 모두 일치
* **Ball**: 숫자는 같지만 위치가 다름
* **Nothing**: 일치하는 숫자 없음
* 3 스트라이크 시 게임 종료
* 각 게임의 시도 횟수를 기록으로 저장

---

## 🧱 프로젝트 구조

```
📁 Project
┣ 📄 main.swift
┣ 📄 BaseballGame.swift
┣ 📄 GameCenter.swift
┣ 📄 RecordManager.swift
```

---

## 📂 클래스별 역할 설명

이 프로젝트는 **각 클래스가 하나의 책임만 가지도록 분리**했습니다.

---

### `main.swift`

**프로그램의 진입점 역할**

* 게임 로직을 직접 다루지 않음
* `BaseballGame`을 생성하고 실행만 담당

```swift
let game = BaseballGame()
game.start()
```

프로그램의 시작 지점을 명확히 하기 위한 구조입니다.

---

### `BaseballGame`

**게임 전체 흐름을 제어하는 클래스**

* 메뉴 출력
* 사용자 입력 처리
* 게임 시작 / 종료 판단
* 다른 객체들을 조합하여 사용

```swift
private var gameCenter = GameCenter()
private var recordManager = RecordManager()
```

계산 로직이나 기록 관리는 직접 처리하지 않고,
각각의 책임을 가진 객체에 위임합니다.

→ 게임의 **흐름과 순서만 책임지는 조율자 역할**입니다.

---

### `GameCenter`

**숫자 야구 게임의 규칙과 계산을 담당하는 클래스**

* 정답 숫자 생성
* 입력값 유효성 검사
* 스트라이크 / 볼 계산

```swift
enum GameResult {
case correct
case nothing
case progress(strike: Int, ball: Int)
}
```

* 계산 결과를 문자열이 아닌 `enum`으로 반환
* 출력은 하지 않고, 결과만 전달

```swift
private func splitNum(_ num: Int)
```

내부 계산 로직은 `private`로 숨겨 외부에서 알 필요 없도록 설계했습니다.

→ 게임 규칙이 변경되더라도 이 클래스만 수정하면 되도록 구성했습니다.

---

### `RecordManager`

**게임 기록과 상태를 관리하는 클래스**

* 시도 횟수 증가
* 게임 결과 저장
* 기록 출력

```swift
private(set) var trialCounts: [Int]
```

* 외부에서는 읽기만 가능
* 수정은 메서드를 통해서만 가능

→ 상태 변경 책임을 명확히 하기 위한 설계입니다.

---

## 🔐 접근 제어 설계 의도

* `private`

* 내부 구현 세부사항 은닉
* `private(set)`

* 상태는 보호하고 읽기만 허용
* 불필요한 `public` 노출 최소화

객체 간 결합도를 낮추고, 책임을 명확히 하기 위함입니다.

---

## ✨ 리팩토링 포인트

* 문자열 비교 대신 `enum` 사용
* 계산 로직과 출력 로직 분리
* Set은 중복 검사 용도로만 사용
* 클래스별 책임을 기준으로 코드 정리

---

## 📝 느낀 점

* 클래스 분리를 통해 코드 가독성이 좋아졌고
* 책임이 명확해지면서 수정 포인트를 찾기 쉬워졌음
* 단순히 동작하는 코드보다 구조를 고민하는 경험을 할 수 있었음
70 changes: 70 additions & 0 deletions juhee/juhee/BaseballGame.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// BaseballGame.swift
// juhee
//
// Created by 김주희 on 1/13/26.
//

import Foundation

public class BaseballGame { // 야구 게임 진행 클래스

private var gameCenter = GameCenter() // 게임 연산 계산 인스턴스 생성
private var recordManager = RecordManager() // 게임 기록을 관리하는 인스턴스 생성

// MARK: - 게임 선택 함수
func start() {

while true {
print("환영합니다!🤗 원하시는 번호를 입력해주세요 💬")
print("1. 게임 시작하기 ⚾️ 2. 게임 기록 보기 📋 3. 종료하기 ⛔️")

switch readLine() {
case "1":
print("\n< Round \(recordManager.trialCounts.count + 1): 게임을 시작합니다 >")
playGame() // 야구게임 진행 메소드 실행
case "2":
print("\n< 게임 기록 보기 📋 >")
recordManager.showRecords() // showRecords 함수 호출
case "3":
print("\n< 숫자 야구 게임을 종료합니다. ⛔️ >")
exit(0) // 강제 종료 함수 실행
default:
print("올바른 숫자를 입력해주세요! 😤")
}
Comment on lines +22 to +34
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readLine()은 옵셔널 값으로 알고있는데, 바인딩 안해주어도 switch문 잘 돌아가나용??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch문에 readLine()을 넣으면 case에 해당하지않는 nil값은 바로 default문이 실행되어서 옵셔널 바인딩 + 값 비교를 동시에 해주는거라 더 깔끔하게 작성할 수 있습니당!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아...!! 옵셔널 타입이라서 case Optional("1")처럼 되어야하지 않나...라고 헷갈렸었습니다ㅠㅠㅠ
맞네요 비교는 바로 되겠네요ㅋㅋㅋ default로 한번에 처리할 수 있다니 좋은 활용법인 것 같습니다! 설명 감사드려요!!☺️

}


// MARK: - 농구 게임 시작 함수
func playGame(){

let answer = gameCenter.makeAnswer() // 정답 만드는 함수 호출
var isplay = true
while isplay { // 입력값 검사 반복문
print("숫자를 입력하세요:")
guard let inputNumber = readLine().flatMap(Int.init), // 올바른 입력값인지 검사
gameCenter.checkInput(inputNumber) // 입력값 검사 함수 호출
else {
print("올바르지 않은 입력값입니다.😤 다시 입력해주세요!\n")
continue
}

recordManager.addTrial() // 올바른 숫자를 입력하였으므로 시도횟수 +1

let result = gameCenter.compare(inputNumber, answer)

switch result {
case .correct:
print("정답입니다!✔️\n")
recordManager.add(recordManager.trial) // 정답이므로 배열에 최종 시도 횟수 입력
recordManager.trial = 0 // 게임 시도 횟수 0으로 초기화
isplay = false
case .nothing:
print("Nothing 😵\n")
case .progress(let s, let b):
print("\(s) 스트라이크 \(b) 볼\n")
}
}
}
}
}
74 changes: 74 additions & 0 deletions juhee/juhee/GameCenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// GameCenter.swift
// juhee
//
// Created by 김주희 on 1/14/26.
//

import Foundation

class GameCenter { // 게임에 필요한 계산을 하는 클래스
var gameNumber = 3

// MARK: - 입력한 세자리 수를 숫자 각 한개씩으로 배열로 쪼개는 내부 로직 함수
private func splitNum(_ num: Int) -> [Int] {
return String(num).compactMap { $0.wholeNumberValue }
}


// MARK: - 정답 만드는 함수
func makeAnswer() -> [Int] {
let arr = (0...9).map { $0 }

let shuffledArr = arr.shuffled() // 배열을 랜덤으로 섞어줌

if shuffledArr[0] == 0 {
return [Int](shuffledArr[1...gameNumber]) // Int 배열로 형변환 필수
} else {
return [Int](shuffledArr[0...gameNumber - 1])
}
}


// MARK: - 사용자가 입력한 값 검증 함수
func checkInput(_ inputNumber: Int) -> Bool {
let set = Set(splitNum(inputNumber))

return set.count == gameNumber // Array를 Set으로 변환하여 중복을 제외한 값이 3이어야 함
&& Int(pow(10.0,Double(gameNumber - 1))) - 1 < inputNumber
&& inputNumber < Int(pow(10.0,Double(gameNumber)))
}


// MARK: - GameResult 구조체
enum GameResult {
case correct
case nothing
case progress(strike: Int, ball: Int)
}


// MARK: - 입력값과 정답을 비교해 힌트 계산하는 함수
func compare(input: Int, with answer: [Int]) -> GameResult {
var strike = 0
var ball = 0
let inputArray = splitNum(input) // 입력값을 쪼개서 세 원소를 가진 배열로

// strike, ball에 결과값 입력
for i in 0..<gameNumber {
if inputArray[i] == answer[i] {
strike += 1
} else if answer.contains(inputArray[i]){
ball += 1
}
}

if (strike == gameNumber) {
return GameResult.correct
} else if (strike == 0 && ball == 0){
return GameResult.nothing
} else {
return GameResult.progress(strike: strike, ball: ball)
}
}
}
32 changes: 32 additions & 0 deletions juhee/juhee/RecordManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// RecordManager.swift
// juhee
//
// Created by 김주희 on 1/13/26.
//

import Foundation

// MARK: - 게임 기록 관리 클래스
class RecordManager { // 기록 관리 클래스
var trial = 0 // 게임 시도 횟수
private(set) var trialCounts: Array<Int> = [] // 시도 횟수 저장할 빈 배열

// 게임 시도 횟수 증가 함수
func addTrial() {
trial += 1
}

// 배열에 시도 횟수 추가 함수
func add(_ trialcount: Int) {
trialCounts.append(trialcount)
}

// 기록 출력 함수
func showRecords() {
for (idx, value) in trialCounts.enumerated() {
print("\(idx + 1)번째 게임: 시도 횟수 - \(value)")
}
}

}
11 changes: 11 additions & 0 deletions juhee/juhee/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// main.swift
// juhee
//
// Created by 김주희 on 1/13/26.
//

import Foundation

let game = BaseballGame()
game.start()