[Swift] 숫자 야구 게임 완성

sonny·2024년 11월 7일
5

TIL

목록 보기
34/48
post-thumbnail

오늘 이제 마무리를 할 시간이다. 내일이면 제출기한이기에..

오늘은 마지막 클래스를 만들고 실행해보면 될 것 같다.


Baseball Class

캡슐화하기

우선 이 클래스는 숫자 야구 게임의 핵심 로직들을 관리하고 있는 클래스여야한다.

앞서 만든 정답 클래스, 입력 클래스, 힌트클래스 이 세 개는

야구게임의 주요기능을 담당하고 있는 클래스이기 때문에 안전하게 캡슐화로 감싸주려한다.

이렇게 하면 클래스 내부에서만 접근이 될 것이다.

근데 내가 방금 private 한 거 그새 까먹고 클래스로 호출하죠?

저거 호출하려면 위에 private 로 한 소문자로 시작하는 answerGenerator 로 해줘야한다.

그리고 나서 게임시작을 알리는 프린트 구문을 넣었는데 더 화려하게 넣고 싶었다.

하.. 이제 게임 시작이 되었으니 순차적으로 할 일을 미리 적어봐야겠다

꼬물 글시 ~~

  • 게임시작 동시 정답 생성 (완료)
  • input 에서 문자열을 받는다
  • 받은 입력 문자열을 배열로 변환 시킨다
  • 힌트 클래스를 이용해 정답과 비교하면서 "스트라이크: 0개, 볼: 0개" 라고 알려준다. 틀리면 "아까워요 다시" 라고 반복 되면서 사용자가 입력할 수 있게 해야한다.
  • 3개 다 맞추면 "홈런" 출력 후 종료.

1. input 에서 문자열을 받기

문자열을 받을 때 부터 맞출 때까지 반복이 되어야하니 while 문으로 돌리다가 끝내면 될 것 같다.

먼저 userInput 이라는 변수를 만어줬다.

그리고 입력 클래스의 메서드를 실행해주면 되는데,

어제 getInput 메서드에서 입력값에 대한 오류정리를 했었다.

(어제 작성한 Inputhandler class 다)

이렇게 많은 테스트를 통과한 숫자만 다음으로 넘어갈 수 있기 때문에 입력이 적절하게 잘 됐다면 countinue 가 실행되어 다음 루프로 넘어갈 수 있다.

만약 저 검문소에서 하나라도 걸리면 nil을 반환하게 되어 숫자를 입력하라고 계속 나올 것이다.

자 여기까지 input에서 문자열을 받는 걸 완료했고,

2. compactMap 뉴규세여.

Input 에서 정수를 받으면 배열로 변환 하는 방법을 알아야했다.

사실 이 부분에서 막혀서 다른 수강생분들은 어찌하는지 너무 궁금했고..

그렇게 다른 분들 코드를 보다가, 예전에 누가 만든 코드까지 보게 되었는데

compactMap? 이라는 걸 사용하길래 궁금해서 찾아봤다.

번역)

이 시퀀스의 각 요소로 주어진 변환을 호출한 비닐 결과가 포함된 배열을 반환합니다.

비닐 결과가 뭐야... 라고 했지만 코드 예시를 보니 찾던거였다.

좀 더 자세한 설명을 보고 싶어서 블로그도 찾아봤는데..

공식문서에 있는 예제로만 사용해서 나도 사용보려한다.


트러블 발생 (while 조건 누락, Character타입의 변환)

여기저기 피가 좀 많이 났는데,,,

우선 공식문서 예제에 있는 { str in Int(str)} 여기서 str가 뭐지? 했는데

그냥 클로저 안에서 정의된 임시 변수 이름이었다.

어차피 각 번호를 배열로 하는거니까 간단하게 Num 이라고 해주고 예시랑 똑같이 썼는데 오류가 났다.

오류를 번역해보니

" 'String.Element' 유형(일명 '문자')의 값을 예상 인수 유형 'String'으로 변환할 수 없습니다 "

그니까.. 내가 지금 문자열을 가지고 온건데 .. string 으로 변환할수 없다는게 무슨 말이야?

튜터님 찾아갔다.
.
.
.

해결

튜터님과의 알차고 멋진 대화를 통해서 수정했고 while 문에서 피 나던 것도 해결했다.

while 문 앞에 컨디션을 넣지 않아서 피가 났던 것이고,

저기 표시한 부분에서 NumCharacter 타입이기 때문에 바로 Int(Num) 으로 변환할 수 없었던 것이다.

IntString 타입만을 받아 정수로 변환하기 때문인데,

왜 String 이 필요할까?

  • Character 타입
    NumCharacter 타입이고, Character 는 문자 하나를 나타내는 타입이지만, 숫자를 나타낼 때도 Int 로 직접 변환할 수 없다.
  • Int 초기화 규칙
    IntString을 인자로 받아 정수로 변환하는데, Character를 직접 받는 초기화 구문은 없어서 String 으로 변환 해줘야한다.

그래서 Int(String(Num))Num를 문자열로 변환한 후, 다시 Int로 변환하는 거다.
.
.
예시)

let userGuess = userInput.compactMap { character in
    Int(String(character)) 
    
    // 'character'를 'String'으로 변환 후 'Int'로 변환
}

만약 userInput"123"이라면,

첫 번째 문자 "1"Character 에서 String("1") 으로 변환한 후 Int("1") 로 바꾼다.

이렇게 "2", "3" 도 각각 Int 로 바꿔 최종적으로 [1, 2, 3] 배열을 얻을 수 있다.

아무튼 이 과정에서 문자열을 배열로 변환을 마치고,


3. 힌트 클래스를 정답과 비교하여 알려주기

앞서 힌트 클래스에서 반환타입을 튜플로로 받았기 때문에,

현 코드에서 hintCalculator.calculateHints

정답 (answer)과 사용자 입력 (userGuess)을 배교해서 스트라이크와 볼을 계산하는 함수이고 ,

두 배열을 비교해 (strike, ball) 튜플을 반환해준다.

calculateHint 함수는 하나의 튜플을 반환하지만, 그 튜플은 두개의 값을 포함하고 있다.

그렇게 받은 튜플을 분해해서 개별적인 값으로 받게 되면 코드가 더 명확해진다고 한다.

그렇게 계산해서 스트라이크와 볼의 갯수를 프린트해주고,

strike 가 3개 모두 맞게 되면 홈련을 프린트 한 후 종료된다.

그렇지 않으면 아까비.. 프린트를 하며 while 루프가 계속 된다.


4. 실행 결과

은근 재밌다.

깃허브 코드 링크


응 아니야, Error처리 따로 해~

에러 공식문서 링크 에서 자세히 설명이 되어있어서 도움이 많이 됐다.

크게 세가지 요소가 있는데, 에러 정의, 에러 던지기 (throwing), 에러 처리가 있다.

Error 프로토콜을 사용하여 에러를 정의하고, throw, try, catch 구문을 통해서

에러를 던지거나 처리할 수 있다.

< Error 정의 >

Swift 에서는 에러를 정의하기 위해 Error 프로토콜을 채택한 타입을 생성하는데,

보통 enum 타입을 사용해서 다양한 에러 상황을 정의한다고 한다.

InputError 라는 enum 을 사용해서 여러 가지 입력 관련에 대한 에러를 깔끔하게 정리할 수 있는 것이다.

< 에러 던지기 >

에러는 throw 키워드를 이용해 던질 수 있다.

함수나 메서드에서 에러를 던질 땐 throw 를 함수선언에 추가해야한다.

코드를 보면 getInput() 메서드가 입력값이 없거나 공백일 때,

trows 가 에러를 던질 수 있다는 걸 나타나는 것이다.

그리고 함수 호출 시 try 를 사용해서 에러처리를 해야한다.

< 에러 처리 >

에러를 처리하려면 두 캣취! (do-catch) 구문을 사용해야한다.

try를 사용해서 함수를 호출하면, 발생한 에러를 catch 블록에서 처리가 가능하기 때문이다.

do 블록에서 try 를 사용해 에러를 던질 수 있는 함수를 호출하고,

catch 블록에서 발생한 에러를 처리한다.

  • 에러 정의 : Error 프로토콜을 채택한 타입으로 에러를 정의한다.
  • 에러 던지기 : throws 키워드를 사용해 함수에서 에러를 던질 수 있다.
  • 에러 처리 : do-catch 구문으로 에러를 처리하고, try 를 사용해 호출한다.
  • 에러 처리 방식 : try?, try!, try 를 통해 에러 처리 방법을 선택한다.

try?, try! 는 한번 더 공부해봐야겠다.


음...

이번에 구현한 클래스는 아무래도 제일 중요한 로직을 처리하는 부분을 담당하고 있다보니

게임 흐름도 관리해야했고, 사용자 입력을 받아 정답과 비교하고 결과를 출력하는 것 까지 완성했다.

그래도 나름 객체지향을 공부했다고 단일 책임 원칙을 준수해서,

각 클래스별로 나누었는데 그래도 이 덕분에 각 클래스의 책임이 분명해졌다고 생각한다.

또 guard let을 사용했는데 이번 과제하면서 정말 제일 많이 사용해본 것 같다.

값이 제대로 입력되지 않으면 다음 반복으로 넘어가다보니 불필요한 오류도 방지되어 좋았다.

그리고 마지막에 튜플을 반환한 걸 분해해서 사용하는 방식이 코드가 좀 더 간결하고 가독성도 좋아지는 걸 알게 되었다.

뭔가 더 직관적으로 보여서 튜플도 마냥 어렵게 봤는데 생각보다 할 만 했던 것 같기도..

계산기 과제를 할 때랑은 다르게 정말 다양한 메서드를 활용했다.

클래스를 분리하고 협력하는 구조가 재미있었고, 많이 고민해고 배운 시간인 듯 하다.

Lv.3 까지 했지만 그래도 자신에게 칭찬 하고 싶다..

그리고 도와주신 튜터님도 너무 감사합니다.

저 말 잘 들었어요.

" 다음에는 예외 처리시 nil이 아니라 다른 방법으로도 시도해보시면 좋을 것 같습니다! "

라고 저번 주 과제 피드백을 확인하고 나서 에러처리 부랴부랴 공부하고 시도했습니다!!!
강의대로 했더니 특별한 에러 없이 잘 작동도 했어요!

profile
iOS 좋아. swift 좋아.

15개의 댓글

comment-user-thumbnail
2024년 11월 7일

정답 : 7 3 5

1개의 답글

과제를 수행하며 겪으셨던 고충이 제 일처럼 느껴지네요...(?)
고생 많으셨습니다~ 결국 원하시던 결과를 얻으신 것이 멋져요!!
compactMap을 사용해보셨군요!! flatMap이라는 친구도 있는데 한번 찾아보시면 어떨까요?
다차원 배열을 사용하신다면 활용하실 일이 있으실 것 같아요~

1개의 답글
comment-user-thumbnail
2024년 11월 8일

레벨 3까지가 제일 어려웠던 게 함정이라면 함정...

2개의 답글
comment-user-thumbnail
2024년 11월 8일

근데 내가 방금 private 한 거 그새 까먹고 클래스로 호출하죠?

저거 호출하려면 위에 private 로 한 소문자로 시작하는 answerGenerator 로 해줘야한다.

이 부분에서 에러난 건 접근수준의 문제가 아니라 AnswerGenerator 타입의 객체가 가진 메서드를 넣은 게 아니라 타입 자체가 가진 메서드를 넣는 코드를 쓰셔서 난 문제 같아요
AnswerGenertor.generate()를 쓰려면 AnswerGenerator 타입 자체가 가진 메서드라는 의미로 generate()에 static이 있어야 하고 그게 아니라면 인스턴스를 생성해서 generate()메서드를 써야 하는 거죠

그러니까 해당 에러는 캡슐화 과정에서 private을 answerGenerate에 붙여서 생긴 문제가 아닌 거 같아요

1개의 답글
comment-user-thumbnail
2024년 11월 8일

아 글고 Character.wholeNumberValue 하면 Int?가 튀나오니 String으로 변환하지 않고도 정수값으로 사용할 수 있긴해요!

2개의 답글
comment-user-thumbnail
2024년 11월 8일

헐 나도 InputError라고 작명했는데 똑같다!!!! 그리고 스탈뜨~!!!!!가 사라져서 아쉽다
저도 에러처리 강의 듣고 적용했어요🤭
근데 깃헙 링크가 안들어가져요

1개의 답글