[Swift] 숫자 야구 게임 갈아엎은 날

sonny·2024년 11월 6일
3

TIL

목록 보기
33/48

갈아 엎었다.

내 기준 그렇다.


1. InputHandler class

if 말고 guard로 ...

전 코드에서는 if else 로 줄줄이 이어져 내려왔다.

하지만 보다보니 (다른 수강생분들이 하는 것과 비교해보니) ,

guard 문을 많이들 사용하더라. 보다보니 왜 쓰는지 알았다.

뭔가 조건이 만족하지 않으면 처리되는 부분이 한눈에 보기 편한 기분이랄까.

그래서 초반은 if let 을 사용해서 readLine() 의 반환 값을 안전하게 언래핑 해주고,

사용자가 입력한 값이 유효한지를 확인했다.

그 뒤로는 guard를 사용하여 입력이 비어있지 않은지 추가로 체크!

이렇게 두 가지 방법을 조합하여 검증하는 건 안정성과 오류 처리를 향상시키는 좋은 방법이다.

~= 연산자 이용

~= 는 스위프트에서 범위를 체크해주는 연산자다.

왼쪽에 있는 범위에 오른쪽 값이 포함되는지를 확인해 주는데,
.
.
.
쉬운예시)

이런식으로 number가 해당범위에 있는지 바로 확인해주니 편하다.

자 여기까지 하고 어제 하지 못한 힌트 클래스를 구현해보려 한다.


2. Hintclass 최종 구현

튜플(tuple) 이용

우선 calculateHints 라는 함수에 두개의 정수 배열매개변수로 먼저 받고,

반환 타입으로는 (strike: Int, ball: Int) 라는 튜플로 받았다.

튜플로 받으면 스트라이크와 볼 개수를 한번에 반환할 수 있기 때문이다.

한번에 반환한다는건.. 하나의 함수에서 여러 값을 동시에 반환한다거나

반환 된 값들을 쉽게 접근하고 분해해서 사용할 수 있는 거다.

저렇게 하면 나중에 실행할 때,

// 사용할 때
let calculator = Hintcalculator()
let result = calculator.calculateHints(answer: [1,2,3], userGuess:[1,3,2])

print(result.strike) // 이름으로도 접근이 가능하다.
print(result.ball)

이런식으로 스트라이크랑 볼이 항상 함께 계산되고 사용이 되기 때문에 튜플로 반환하는 것이 적절했다.

스트라이크 계산법 (zip, map, reduce 활용)

전체적으로 크게크게 묶어서 보자면,

zip 을 이용해 위에서 지정한 두 배열 (answer, userGuess) 을 순서대로 쌍으로 묶어줬다.

zip 은 저번 알고리듬 문제를 풀 때 배웠는데 쌍으로 순서대로 묶어주니 활용해봤다.

[ zip ]

answer [1,2,3], userGuess [1,3,2] 라고 가정한다면,

-> [(1,1),(2,3),(3,2)] 이런식으로 묶어버린다.

그렇다면 zip 을 이용해 묶었으니,

[ map ]

map 을 이용해 각 쌍을 순회하면서 같은 위치의 숫자를 비교하게 된다면?

-> [(1,1), (2,3), (3,2)][1, 0, 0]

이렇게 원 스트라이크를 확인할 수 있는 형태가 된다.

그리고 삼항연산자 를 사용해서 만약 두 쌍이 같으면 1, 다르면 0 을 반환하게 했다.

근데 스트라이크를 확인했다고 끝이 아니기 때문에,

return 값에 스트라이크 갯수를 더한 모든 값이 계산되어야한다.

그리고 이 방금까지 한 모든 것들은 map 의 클로저를 사용해

각 요소들을 어떻게 변환할지를 정의하는 것이라 보면 된다.

[ reduce ]

.reduce(0,+) 을 써서 배열의 모든 숫자를 더해주어야한다.

(0,+) 은 배열의 순서가 0에서부터 시작해서 모든 값을 더한다는 의미이다.

오늘 마침 아침에 푸는 알고리듬 문제 중에 짝수의 합 을 구하는 문제가 있었다.

그때 내가 reduce 라는걸 처음 이용했는데,

알고리듬을 풀면서 알게 된 메서드나 고차함수를 이용할 때마다 기분이 좋다.


볼 계산법 (filter 사용)

첫 줄부터 보자면,

userGuess.filter {...} 부분은 사용자가 추측해서 적은 숫자 배열에서,

정답 배열에 포함된 숫자만 필터링 한다는 것이다.

처음에 난 스트라이크의 경우도 어쨋든 맞는 번호를 걸러내야하는 행위는 같으니까

스트라이크에도 filter 를 사용해야하는 것 아닌가? 라는 의문을 가졌었다.

하지만 확인해야하는 것에 대해 엄연히 다른 점이 존재했다.

스트라이크의 경우 :

  • 위치와 값이 모두 같아야함
  • 그래서 zip을 사용해 같은 인덱스끼리 쌍을 만들어서 비교하는 것

볼의 경우 :

  • 값만 같고 위치는 달라도 됨
  • 그래서 filter로 값의 포함 여부만 확인해도 되는 것.

결론은 만약 내가 의문을 가졌던 filter로 스트라이크를 구현하게 된다면,

인덱스를 직관적인 면에서도 보기가 어렵고, 의도가 덜 명확하다는 결론이 났다.

목적에 따라 가장 알맞는 메서드를 이용하는 것, 그게 중요한 것 같다.

클로저를 사용해 coatains 이용

변역 해보면

"시퀀스에 주어진 요소가 포함되어 있는지 여부를 나타내는 부울 값을 반환합니다."

라고 한다.

여기서 보면 filter 다음에 클로저 문법이 사용되는 걸 볼 수 있다.

filter 메서드는 각 요소를 검사하는 과정에서 사용할 조건을,

클.로.저 로 받기 때문이다.

기억해야하는 맨날 잊는다...

아무튼 쉽게 본다면
.
.
.

filter : 내가 걸러줄테니까 조건 좀 알려줘.

클로저 : 그래, 스트라이크에서 사용자가 추측한 숫자들 중에서

그 각각의 숫자를 gessedNumber 라고 부를게.

gessedNumber 가 정답 배열에 들어있는지 확인만 할거야

.
.

이런 느낌이다.

contains는 정답(answer)배열 안에 guessedNumber가 포함되는지 확인해주는 것.

그 배열내의 어떤 값과 일치하는지를 체크해주는 것이라고 보면된다.

그렇게 체크한 걸 .count 로 필터링 된 숫자들의 갯수를 세어준다. (숫자를 갯수화 시킴)

이 시점에서는 이제 스트라크와 볼이 모두 포함된 갯수인 것이다.

그래서 - strikeCount 를 해주어야 같은 숫자가 있지만 위치가 다른 경우만 남게 되는 것이다.

왜 근데 스트라이크 카운트를 빼줘야할까.

이유는 filter는 단순히 이 숫자가 정답 배열에 있는가. 만 검사해서 그렇다

answer = [1, 2, 3]
userGuess = [1, 3, 2]

// "사용자가 입력한 숫자들 중에서 정답 배열에 있는 모든 숫자"를 찾음
userGuess의 각 숫자를 검사해보면

1: 정답 배열에 있나? → 있음! (스트라이크)
3: 정답 배열에 있나? → 있음! (볼)
2: 정답 배열에 있나? → 있음! (볼)

// 이 시점에서 .count를 하면 3이 나옴
// 1(스트라이크), 3(볼), 2(볼) 모두 정답 배열에 있기 때문.

이러한 이슈로 스트라이크를 빼주어야 볼 갯수만 확인하여 사용자가 정답을 추측할 수 있게 된다.

정리하자면,

  • 스트라이크인 숫자도 당연히 정답 배열에 있는 숫자
  • 볼인 숫자도 정답 배열에 있는 숫자
  • 따라서 filter 후의 count(스트라이크 + 볼)의 개수가 됨
  • 여기서 strikeCount를 빼면 진짜 볼의 개수만 남게 됨

filter 이 메서드는.... 그냥 "이 숫자가 정답에 있나 없나" 만 체크하기 때문에,

스트라이크든 볼이든 상관없이 모든 매칭되는 숫자를 세게 된다.

그래서 나중에 스트라이크 개수를 빼줘야 실제 볼의 개수를 구할 수 있는 것.

그리고 마지막 return 을 보면 계산된 스트라이크의 볼의 개수를 튜플로 반환한다.
.
.
.

이유는,

위에서 함수의 반환타입을 (strike: Int, ball: Int) 로 정의해 뒀으니

이 함수는 두개의 정수를 포함하는 튜플을 반환해야 한다는 의미라서 그런 것이다.

그래서 함수내에서 return (strikeCount, ballCount) 처럼

튜플 형태로 묶어 반환한 것.


Hintclass 최종 구현

실행하고 숫자를 지정해준 뒤 프린트를 해보았다.

스트라이크와 볼의 갯수가 잘 나오는 걸 볼 수 있었다.

최종구현에서 보다시피 나는 여러 고차함수들을 사용했는데, 개인적인 평을 내려보자면

  • zip, map, reduce, filter 등의 고차함수를 효과적으로 활용
  • 각 계산 과정이 명확하게 분리되어 있어 이해하기 쉽다.

이 정도가 될 듯 하다.


음...

사용자의 입력과 그리고 정답을 비교해서 스트라이크와 볼의 갯수를 계산하는 기능을 구현하는건

사실 지금 내 실력에서는 정말 까다롭고 어려운 수준이었다.

그래서 하나하나 공책에 정리를 해보았고, 그에 맞는 메서드와 고차함수를 이용하는 과정이 쉽진 않았다.

이번 구현 작업을 통해서 아무래도 고차함수를 좀 많이 사용했던터라

배열을 처리하는데 어떤 방법이 좀 더 효율적인 방법인지를 알게 되었고,

또한 튜플을 사용하는 이유가 여러 값을 한 번에 반환할 수 있기 때문이라는 것도 배웠는데

이렇게 하면 호출하는 쪽에서도 쉽게 구분이 가능하니 편하다는 장점 또한 알게 됐다.

이번 과제에서는 수강생들이 여러 메서드를 활용해볼 수 있게끔 하려는 좋은 의도가 보여서 힘들지만 재밌게 만들었다.

내일은 마지막 baseballGame 클래스를 만들어 볼 것이다.

profile
iOS 좋아. swift 좋아.

1개의 댓글

comment-user-thumbnail
2024년 11월 8일

저 이거 보고 ~= 연산자 따라 썼어요 잘했죠?

답글 달기