Graphics Programming

[Codingame] Onboarding 게임 - const와 커링의 미스터리 본문

Season 1/하스켈

[Codingame] Onboarding 게임 - const와 커링의 미스터리

minseoklee 2014. 10. 4. 14:28
페이스북에서 알게 된 사이트인데 코딩으로 게임을 플레이할 수 있다. 되게 신기함 ㅋㅋ 한 마디로 온라인 저지의 게임 버전이다.

게임은 지가 알아서 돌아가는데 예시 코드에는 콘솔 입출력이 있어서 헷갈렸는데, 주석을 읽어보니 콘솔 입출력으로 게임을 제어하는 형식이었다. 제일 첫 번째 게임인 Onboarding을 깨봤다.

게임 화면에 DEBUG MODE 버튼을 눌러보면 적들의 이름이 나온다. 네 개는 HotDroid이고 마지막 것은 Buzz인데 우리 우주선은 HotDroid만 잡고 Buzz에게 죽는다.

-- 게임에서 제공하는 기본 코드
import System.IO

import Control.Monad


main :: IO ()

main = do

    hSetBuffering stdout NoBuffering -- DO NOT REMOVE

    

    -- The code below will read all the game information for you.

    -- On each game turn, information will be available on the standard input, you will be sent:

    -- -> the total number of visible enemies

    -- -> for each enemy, its name and distance from you

    -- The system will wait for you to write an enemy name on the standard output.

    -- Once you have designated a target:

    -- -> the cannon will shoot

    -- -> the enemies will move

    -- -> new info will be available for you to read on the standard input.

    

    loop


loop :: IO ()

loop = do

    input_line <- getLine

    let count = read input_line :: Int -- The number of current enemy ships within range

    

    forM [1..count] $ const $ do

        input_line <- getLine

        let input = words input_line

        let enemy = input!!0 -- The name of this enemy

        let dist = read (input!!1) :: Int -- The distance to your cannon of this enemy

        return ()

    

    -- hPutStrLn stderr "Debug messages..."

    

    -- The name of the most threatening enemy (HotDroid is just one example)

    putStrLn "HotDroid"

    

    loop


예시 코드를 보니 putStrLn "HotDroid"가 있고 하단의 로그에는 다음 메시지가 출력된다.

Game information:

HotDroid has been targeted

Threats within range:

HotDroid 40m

HotDroid 40m

Buzz 70m

Standard Output Stream:

HotDroid


즉 적의 이름과 거리가 입력으로 들어오니 가장 가까운 적의 이름을 출력하라는 것이다. 그러면 우리 우주선이 그 적을 쏠 것이다. 하지만 Buzz는 출력을 하지 않으니 Buzz를 절대 겨냥하지 않고 따라서 죽어버리는 것이다. 그럼 Buzz가 가까이 오면 Buzz를 출력하면 되겠네. 예시 코드에는 이미 적의 목록을 읽어와서 이름과 거리를 추출하는 부분이 들어있다.

    forM [1..count] $ const $ do

        input_line <- getLine

        let input = words input_line

        let enemy = input!!0 -- The name of this enemy

        let dist = read (input!!1) :: Int -- The distance to your cannon of this enemy

        return ()


forM은 이름을 보니 무슨 함수인지 대충 짐작이 가지만 확실히 알아두기 위해 Hoogle에서 타입을 살펴보면

forM :: Monad m => [a] -> (a -> m b) -> m [b]


[a] = [1..count]이니 두 번째 인자는 이 리스트의 각 숫자를 IO b로 평가하는 함수다. 마지막으로 IO [b]를 반환하는데 위의 예시 코드를 보면 빈 튜플 ()을 반환하므로 이 경우에는 forM :: [Int] -> (Int -> IO ()) -> IO [()] 가 되는 걸 볼 수 있다.

그런데 forM [1..count] $ const $ do 가 해석이 잘 안 된다. 이게 무슨 뜻일까? 일단 const :: a -> b -> a 는 인자 두 개를 받아 첫 번째 인자를 그대로 반환하는 함수다. const의 인자 x와 y는 위 코드에서 무엇에 대응하는 걸까? do 표기라 헷갈리니까 bind 연산자로 바꿔보자.

do

        input_line <- getLine

        let input = words input_line

        let enemy = input!!0 -- The name of this enemy

        let dist = read (input!!1) :: Int -- The distance to your cannon of this enemy

        return ()



getLine >>= (\ input_line -> return ())
  where
    input = words input_line
    enemy = input !! 0
    dist = read (input !! 1) :: Int


이걸 forM에 넣어보면...

forM [1..count] (const $ f) where
  f = getLine >>= (\ input_line -> return ())
  input = ... 


일단 forM의 인자는 맞췄다. const는 인자 두 개를 받는데 왜 하나밖에 없지? const $ f는 일단 const f이니까 이걸 하나의 함수 g로 보면

g :: Int -> IO ()
forM [1..count] g where
  ...


1..count 중의 한 숫자를 n이라고 하면 g n 으로 호출될 것이다. g = const f 이니까 끼워넣어보면... g n = const f n = f 올ㅋ 카운트 변수를 무시하기 위한 인자 생략(point-free) 방식이었다. 사실 인자 생략은 아직도 잘 모르겠다 열라 헷갈림

이제 forM 분석은 끝났고 return ()을 return (enemy, dist)로 바꿔서 적들에 대한 정보를 얻는다. 이 정보는 IO [(String,Int)] 타입인데 일단 리스트를 취하면 가장 가까운 적을 반환하는 함수를 작성해보자.

-- 간단한 선형 검색
closest :: [(String, Int)] -> String

closest (x:xs) = sub x xs

    where

        sub :: (String, Int) -> [(String, Int)] -> String

        sub x [] = fst x

        sub x@(xName,xDist) (y@(yName,yDist):ys) =

            if xDist < yDist

                then sub x ys

                else sub y ys


forM으로 받은 것은 IO 모나드가 감싸고 있어서 closest 함수를 바로 적용할 수가 없다. 하지만 bind 연산자의 타입을 생각해보면 그런 함수를 쉽게 작성할 수 있다.

-- 기본 형태
(>>=) :: Monad m => m a -> (a -> m b) -> m b

-- IO의 경우
(>>=) :: IO [(String,Int)] -> ([(String,Int)] -> IO b) -> IO b

-- 마지막에 putStrLn으로 출력할 거니까 b는 ()이 된다.
(>>=) :: IO [(String,Int)] -> ([(String,Int)] -> IO ()) -> IO ()

-- 그러면 예시 코드의 putStrLn을 이렇게 바꿀 수 있다.
enemies >>= (\ list -> putStrLn $ closest list)


그래서 게임을 깬 최종 코드

import System.IO

import Control.Monad


main :: IO ()

main = do

    hSetBuffering stdout NoBuffering -- DO NOT REMOVE

    loop


closest :: [(String, Int)] -> String

closest (x:xs) = sub x xs

    where

        sub :: (String, Int) -> [(String, Int)] -> String

        sub x [] = fst x

        sub x@(xName,xDist) (y@(yName,yDist):ys) =

            if xDist < yDist

                then sub x ys

                else sub y ys


loop :: IO ()

loop = do

    input_line <- getLine

    let count = read input_line :: Int -- The number of current enemy ships within range

    

    let enemies = forM [1..count] $ const $ do

        input_line <- getLine

        let input = words input_line

        let enemy = input!!0 -- The name of this enemy

        let dist = read (input!!1) :: Int -- The distance to your cannon of this enemy

        return (enemy, dist)

    

    -- hPutStrLn stderr "Debug messages..."

    

    -- The name of the most threatening enemy (HotDroid is just one example)

    enemies >>= (\ list -> putStrLn $ closest list)

    

    loop


Comments