haskell study 10

31
Haskell Study 10. Baseball Game

Upload: nam-hyeonuk

Post on 17-Jan-2017

412 views

Category:

Software


1 download

TRANSCRIPT

Haskell Study

10. Baseball Game

Baseball game

이제 Haskell로 간단한 프로그램을 만들 수 있는 준비는 모두 마쳤습니다. 연습삼아 Baseball game

을 콘솔로 만들어봅시다.

Baseball Game 규칙은 다음과 같습니다.

- 정답은 서로 겹치는 자릿수가 없는 세자리의 숫자입니다(123, 549, 608 등).

- 플레이어는 정답이 뭔지 추측해야합니다.

- 정답과 플레이어가 낸 답을 비교해서, 각 자릿수에 대해 숫자와 위치가 모두 일치할 경우 Strike,

숫자만 일치할 경우 Ball이 됩니다. (ex - 정답이 123일 때 124는 2S 0B, 231은 0S 3B)

- 정답을 맞추면 게임이 끝납니다.

Start

우선 적당한 파일을 만들고(여기서는 baseball.hs) main 함수부터 작성해봅시다.

import Data.List

import System.Random

import Control.Monad

main = do

print "hello!"

나중에 쓸 필요가 있는 함수들을 우선 import 해봅니다. main함수를 적당히 작성하고 컴파일되는

지부터 확인해봅시다.

Make Answer

Baseball game을 시작하려면 우선 정답을 생성해야합니다. 게임을 할 때마다 항상 정답이 같으면

게임을 하는 의미가 없으니 정답은 랜덤으로 생성해야겠죠. 정답을 생성하는 함수 makeAnswer를

만들어봅시다. makeAnswer 함수의 타입 서명은 다음과 같습니다.

makeAnswer :: StdGen -> Int

StdGen은 System.Random에 있는 타입으로, 랜덤 값을 생성하기 위한 시드입니다. getStdGen

함수를 통해 얻어올 수 있습니다(getStdGen :: IO StdGen). main 함수에서 StdGen을 생성해

봅시다.

main = do

gen <- getStdGen

let answer = makeAnswer gen

print answer -- print 함수는 putStrLn . show와 같습니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

randomRs 함수는 숫자 범위와 StdGen

값을 받아서 해당 범위 내의 있는

랜덤 값들로 이루어진 무한 리스트를

반환합니다. 이 무한 리스트로부터 정답이

될 수 있는 후보군(정답이 가져야할 조건을

만족하는 값들)을 만들고 그 후보군에서 맨

첫 번째 값을 정답으로 삼습니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

candidates가 바로 후보군입니다.

여기에는 후보가 될 수 있는 모든 값들이

포함되지만, Haskell의 laziness 특성에

의해 모든 후보군을 구하지 않고 제일

첫 번째 후보군만 구하게 됩니다(head

candidates). 여기서 정답이 될 후보군을

구하는 과정은 비결정적 연산(각 자릿수에

들어갈 수 있는 모든 경우의 수를 다

따진다. 어떤 것이 정답이 될 지 모른다.)

이므로 list monad를 사용합니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

h는 100의 자리에 해당하는 자릿수 입니다

무한 리스트인 rands로부터 하나씩 값을

꺼내서 해당 값이 조건을 만족하는지

테스트합니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

guard 함수는 Control.Monad에 있는

함수로, list comprehension에서 술어가

사실 guard를 이용해 구현됩니다. 우선은

조건이 만족되지 않으면 다음 연산으로

넘어가지 못하게 막는, 조건을 만족해야만

다음 연산을 수행하게 만드는 거라고

생각합시다. 정확한 구현이 궁금하다면

Monoid 및 guard의 구현 코드를

살펴보시는 걸 추천드립니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

h(100의 자리)값은 구했으니, 다음은 10

의 자리 값 t를 구해봅시다. 모든 자릿수는

겹치면 안되기 때문에 guard (h /= t)

조건이 붙었습니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

1의 자리(o) 역시 마찬가지 입니다. 10의

자리, 100의 자리 값과 겹치면 안되므로

이를 guard를 통해 구현했습니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

모든 조건을 만족하는 h, t, o 값에 대해

이를 세자리의 숫자로 만듭니다. 이 결과로

candidates에는 조건을 만족하는 모든

정답 후보 숫자가 들어가게 됩니다.

Make Answer

이제 makeAnswer 함수의 구현입니다. 코드부터 본 다음 한줄씩 이해해봅시다.

makeAnswer :: StdGen -> Int

makeAnswer gen = head candidates

where rands = randomRs (0, 9) gen

candidates = do

h <- rands

guard (h /= 0)

t <- rands

guard (h /= t)

o <- rands

guard (o /= t && o /= h)

return (h*100 + t*10 + o)

그리고 정답은 그 후보군에 속하는 숫자

중 제일 첫 번째 숫자입니다. laziness

에 의해 무한 리스트를 모두 계산하지

않고 후보군이 하나라도 나오면 나머지는

계산하지 않고 바로 그 답을 리턴합니다.

reads

이제 입력받는 함수를 구현할 차례입니다. 우선 reads 함수에 대해 짚고 넘어갑시다. reads 함수는

read함수가 parsing에 실패할 경우 예외를 발생하는 것과 달리 예외를 발생시키지 않습니다. 좀

더 안전한 함수라고 볼 수 있습니다. reads 함수는 String -> [(a, String)] 이라는 타입을 갖고

있습니다. 주어진 String으로부터 읽고자 하는 타입이 a, parsing되지 못한 나머지 부분이 튜플의 두

번째 원소 String으로 들어갑니다. parsing에 실패할 경우 []를 리턴합니다.

Prelude> reads "123abc" :: [(Int, String)]

[(123, "abc")]

Prelude> reads "abc123" :: [(Int, String)]

[]

Prelude> reads " 12" :: [(Int, String)]

[(12, "")]

getInput

사용자로부터 입력받은 값(문자열)로부터 baseball game의 input을 얻어내는 함수를

만들어봅시다. 잘못된 입력이 있을 수 있으므로 성공할 경우 Just input을, 그렇지 않을 경우

Nothing을 리턴하도록 만듭시다.

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

우선 reads 함수를 이용해

raw 값을 읽은 다음,

go 함수를 이용해 해당

입력이 조건을 만족하는지

테스트합니다.

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

텅 빈 리스트가 나왔다는

건 parsing에 실패한

경우이므로 잘못된

입력입니다. Nothing을

리턴합시다.

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

리스트 내에 원소가 하나고,

튜플에서 두번째 원소가 텅

빈 리스트면 일단 입력으로

숫자 하나가 들어왔다는

뜻입니다. 이제 이 숫자가

정당한 입력 조건을

만족하는 지 봅시다.

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

저는 여기서 Input이 0이

들어올 경우 프로그램이

종료되게 만들려고

했기 때문에 값이 0인

경우도 정당한 입력으로

취급했습니다.

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

그 외의 경우엔 값이

세자리수가 아니라면

잘못된 것이겠죠. Nothing

을 리턴합니다.

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

숫자가 세 자리수고, 하나도

겹치는게 없다면 그 값은

정당한 입력입니다. Just i

를 리턴합니다.

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

그 외의 경우는 무조건

Nothing을 리턴하는게

맞겠죠(중복되는 자릿수가

있는 경우).

getInput

getInput :: String -> Maybe Int

getInput raw = go (reads raw)

where go [] = Nothing

go [(i, [])]

| i == 0 = Just 0

| i > 999 || i < 100 = Nothing

| (length . nub . show) i == 3 = Just i

| otherwise = Nothing

go _ = Nothing

패턴 매칭에서 나머지

경우에는 역시 정당한

입력이 아닐 것이므로

Nothing을 리턴합니다.

play

다음은 play 함수입니다. 이 함수는 실제 게임 플레이 부분을 담당합니다.

main = do

gen <- getStdGen

play $ makeAnswer gen --정답을 생성한 후 정답 값을 이용해 게임 플레이를 합니다.

play :: Int -> IO ()

play answer = do

putStrLn "guess the answer!(0 : exit)"

rawInput <- getLine

let input = getInput rawInput

case input of Just 0 -> end

Nothing -> retry answer

Just guess -> check answer guess

play

let input = getInput rawInput

case input of Just 0 -> end

Nothing -> retry answer

Just guess -> check answer guess

핵심은 위 코드입니다. getInput 함수를 통해 input값을 가져온 후, input 값이 Just 0이라면

종료이므로 end 함수를 호출, Nothing이라면 잘못된 입력이므로 다시 입력을 시도(retry), 제대로

된 입력이 들어왔다면 정답이 맞는지 아닌지 확인해야하므로 check 함수를 호출합니다.

이제 이 3개의 함수만 구현하면 됩니다.

end

end :: IO ()

end = do

putStrLn "game over."

end 함수는 간단합니다. 그냥 프로그램 종료에 해당하는 문구를 출력한 후 프로그램을 종료시킵니다.

만약 다른 어떤 작업을 하고 싶다면 이 함수에 어떤 동작을 추가하면 되겠죠.

retry

retry :: Int -> IO ()

retry answer = do

putStrLn "invalid input."

play answer

retry 함수는 단순히 잘못된 입력이 들어왔음을 화면에 출력한 후, play 함수를 다시 호출합니다.

play 함수를 다시 호출해서 입력부터 다시 시작하는 거죠.

check

check :: Int -> Int -> IO ()

check answer guess

| answer == guess = do

putStrLn "you are right!"

end

| otherwise = do

let (s, b) = getStrikeAndBall answer guess

putStrLn $ show s ++ " strike, " ++ show b ++ " ball"

play answer

check 함수는 정답을 맞췄을 경우, 아닐 경우 두 가지가 있습니다. 정답을 맞췄을 경우 정답임을

출력하고 종료, 그렇지 않을 경우 strike, ball 개수를 출력한 후 play 함수를 호출함으로써 다시

정답을 추론하게 만듭니다.

getStrikeAndBall

getStrikeAndBall :: Int -> Int -> (Int, Int)

getStrikeAndBall answer guess = (strike, ball)

where a = show answer

g = show guess

strike = length $ filter (\(x,y) -> x == y) (zip a g)

ball = (length $ filter (\x -> x `elem` a) g) - strike

getStrikeAndBall 함수는 추측 값과 정답이 주어졌을 때 strike, ball 개수를 튜플 형태로

반환합니다. 로직은 단순한 편이니 쉽게 이해할 수 있을거에요. strike는 answer와 guess를 zip한

후 튜플의 두 요소가 일치하는 개수를, ball은 guess의 자릿수중 answer에 포함되는 이들의 개수를

구한 후 거기서 strike 개수만큼 제외합니다.

result

작성한 코드를 컴파일해서 실행해보면 다음과 같은 결과를 얻을 수 있습니다.

guess the answer!(0 : exit)

123

0 strike, 1 ball

guess the answer!(0 : exit)

111

invalid input.

guess the answer!(0 : exit)

456

1 strike, 0 ball

guess the answer!(0 : exit)

789

0 strike, 1 ball

guess the answer!(0 : exit)

159

1 strike, 0 ball

guess the answer!(0 : exit)

258

1 strike, 0 ball

guess the answer!(0 : exit)

357

you are right!

game over.

more..

baseball 게임에 다음 요소들을 추가해봅시다.

• 턴 수 출력

정답을 맞추는데 까지 몇 턴이 걸렸는지를 출력해봅시다.

• 다시 시작

정답을 맞추면 프로그램을 종료하지 말고, 새로운 정답을 기반으로 다시 플레이를 하게 만들어봅시다.