. 몸에도 좋고 맛도 좋은 (백준 3190 : 뱀)
본문 바로가기
프로그래밍 공부/백뚠

몸에도 좋고 맛도 좋은 (백준 3190 : 뱀)

by 불냥이_ 2020. 12. 22.

3190번: 뱀 (acmicpc.net)

 

3190번: 뱀

 'Dummy' 라는 도스게임이 있다. 이 게임에는 뱀이 나와서 기어다니는데, 사과를 먹으면 뱀 길이가 늘어난다. 뱀이 이리저리 기어다니다가 벽 또는 자기자신의 몸과 부딪히면 게임이 끝난다. 게임

www.acmicpc.net

라떼에는 도트로 된 게임기가 있었는데, 거기에 항상 뱀 게임이 있었다.

(테트리스, ㅗ 모양으로 된 자동차 게임, 핑퐁 게임도 같이 있었다... 기억나는 사람 있는가? 내 또래는 알거야...)

 

그 뱀 게임을 이제 만들어 볼 시간이다. 

 

일단 입력부터 받아보자.

import sys
from collections import deque 

sys.stdin = open("input.txt", "r")

# 보드의 크기 N
N = int(sys.stdin.readline()) + 2

# 사과의 갯수 K
K = int(sys.stdin.readline())

# 사과 위치 apple
apple = [list(map(int,sys.stdin.readline().split())) for _ in range(K)]
for i in range(len(apple)):
    apple[i] = [apple[i][0], apple[i][1]]

# 사과 = 2
for i in range(len(apple)):
    cage[apple[i][0]][apple[i][1]] = 2

# 변환 횟수 L
L = int(sys.stdin.readline())

# [시간, 방향]
cmd1 = deque()
cmd2 = deque()
for i in range(L):
    cmd1_ipt, cmd2_ipt = sys.stdin.readline().split()
    cmd1.append(cmd1_ipt)
    cmd2.append(cmd2_ipt)
cmd1 = deque(map(int, cmd1))

(크기에 +2를 해준 것이 보이는가? 이것은 밑에서 설명하겠다.)

 

다른 것은 평범하게 받아왔지만, 시간과 방향을 받아오는 cmd는 불편하게 cmd1과 cmd2로 나뉘어서 받아온 것을 볼 수 있다.

 

다름이 아니라, readline()으로 받아오면 문자열 비교가 안되는 일이 있었다. 그래서 고민 끝에 나눈 다음에 시간 부분은 정수형 처리를 해서 넣었는데, 알고보니 rstrip()을 하지않아 보이지 않는 공백이 있었던 것 같다. 

이제부터 rstrip()을 생활화합시다.

 

하지만 문자열로 받아와도 문제인 것이, 시간(sec)은 정수형이기 때문에 비교를 할려면, 시간을 따로 문자열로 바꿔주거나 cmd = [시간, 방향] 에서 시간만 정수형으로 처리해야 하는데, 받아올 때 정수형/문자형을 따로 처리해서 한꺼번에 리스트에 삽입할 수 있는 방법이 있나? 

 

위처럼 따로따로 받으면 될 것 같긴 하지만, 실제 서비스에서 둘이 묶여져있지 않으면 예기치 않은 오류때문에 cmd1은 처리되었는데, cmd2가 처리되지 않는 상황이면 버그를 야기할 것 같다.

 

무튼 다음으로 넘어가보자.

 

 

그리고, 우리 귀여운 뱀이 뛰놀수 있는 NxN의 이차원 배열을 만들어주자.

이 우리 안에서 뱀은 열심히 사과를 먹으러 갈 것이다.

cage = [[0 for col in range(N)] for row in range(N)]

 

우선, 이 문제의 카테고리는 큐 이므로 뱀은 큐로 구현한다.

그리고 시작 위치는 [1,1] 이기 때문에 1,1을 추가해주자.

 

왜 0,0 이 아니라, 1,1인가? 

위에서 입력을 받아올 때, N에 2를 더해준 것이 기억 나는가? 

1행1열이 [1,1]이 되기 위해서 N에 +2 를 해주었다. 사실 +1만 해주어도 되지만, 실제 게임에서는 울타리도 그래픽으로 구현해줘야하기 때문에 +2 를 넣었다.

 

# 초
sec = 0

# 뱀
snake = deque()
snake.append([1,1])

# 방향전환
direction = 1
# 위, 오, 밑, 왼
dx = [-1, 0, 1, 0]
dy = [0, 1, 0, -1]


# 커맨드
breaker = False

 

그 다음엔 방향전환이다.

진행방향 왼쪽 (L) 을 받을 때, -1, 오른쪽(D)을 받을 때, +1 해서 방향전환이 될 수 있도록

dx = [-1, 0, 1, 0]

dy = [0, 1, 0, -1]

로 지정해준다. 

 

direction이 1이면 위, 2면 오른쪽, 3이면, 밑, 4이면 왼쪽을 향하게 되며, L을 받으면 -1, R을 받으면 +1 이 되어 방향전환이 된다. (기본 방향은 오른쪽이므로 1이 된다.)

 

breaker는 밑에서 설명하겠다. (뱀이 죽었을 때를 위한 코드이다.)

 

 

while True:
    # 시간
    sec += 1
    # print("sec:", sec)
    
    # 머리 움직임
    snake.appendleft([snake[0][0], snake[0][1]])
    
    # 움직일 좌표
    snake[0][0] = snake[0][0] + dx[direction] # x
    snake[0][1] = snake[0][1] + dy[direction] # y

 

뱀은 죽지 않는 이상 계속 움직여야하는 부지런한 친구이기 때문에 while True:로 설정했다.

 

우선 시간 (일종의 턴의 개념이 된다.) 이 늘어나는 것으로 턴을 시작한다. 그리고 중요한 움직임을 순서대로 넣자.

가장 중요한 움직임이라고 하면 머리가 움직이는 것일 것이다.

 

뱀을 deque로 구현했던 것을 기억하는가? snake(deque) = [0]은 뱀의 머리의 좌표가 되고, 나머지는 몸통의 좌표가 된다.뱀이 움직이면 새로운 머리의 좌표가 왼쪽으로 들어오게 된다. 

 

즉, sec = 1에서 뱀이 머리를 늘리는 순간, snake 는 [[1,1],[1,1]] 가 되고,

 

그 다음, 머리에 해당하는 좌표를 새로운 좌표로 만들어준다.

그러면 snake는 [[1,2],[1,1]] 된다.

(이 과정을 한꺼번에 수행할 수 있을 것 같지만, 일단은 나눠서 진행하겠다.)

 

만약 사과를 먹는다면 이대로 진행하겠지만, 사과를 먹지 않는다면 몸통에 해당하는 [1,2]를 삭제할 것이다.

 

 

이제 움직였을때, 중요한 움직임은 뱀이 죽는지 안죽는지를 판별하는 것이다.

    # 벽 부딪힘
    if snake[0][0] < 1 or N - 1 <= snake[0][0] or  snake[0][1] < 1 or N - 1 <= snake[0][1]:
        break

    # 몸에 부딪힘
    if len(snake) > 2:
        
        for i in range(2, len(snake)):
            if snake[0] == snake[i]:
                breaker = True
                break
        if breaker == True:
            break

전체 크기를 N+2로 만들었던 것이 기억나는가? 

그렇기 때문에 벽이 있는 위치는 x, y= 0 이랑 N-1이 된다. 거기에 뱀의 머리가 들어온다면 사망처리를 해준다.

 

그 다음은 몸통에 해당한다. 

 

몸에 부딪히는 것은 길이가 2 이상일 때로 설정했지만, 실제로 뱀이 자신의 몸에 부딪힐려면 snake의 길이가 4이어야한다.

(길이가 4이면 움직이는 찰나에 몸을 뻗기때문에 순간적으로 길이가 5가 되어서 자신의 몸통에 부딪힐 가능성이 생긴다. 그리고 아직 뱀이 머리를 뻗은 후, 꼬리를 땡기는 과정을 수행하지 않았던 것을 기억하자. 지금은 꼬리를 땡기기 전인, 몸통의 길이가 순간적으로 늘어나는 과정 안이다.)

 

그래서 몸통 부분에 해당하는 range(2, len(snake))에서 이동한 머리와 똑같은 좌표가 있으면 사망처리한다.

 

그리고 이 전에 breaker = False했던 것을 기억하는가?

여기서 for문을 빠져나간 뒤, while까지 지우기 위한 포석이었다.

 

더 깔끔한 방법이 있을 것 같지만 (예를들어 뱀의 머리과 몸통에 해당하는 좌표를 맵 상에서 False로 해주고, 머리가 False인 곳으로 들어가면 사망처리 한다거나. 사실 이게 게임에 더 부합하는 방식인 것 같다.) 일단 이렇게 해주겠다.

 

    # 사과 먹음
    if cage[snake[0][0]][snake[0][1]] != 2:
        snake.pop()
    else:
        cage[snake[0][0]][snake[0][1]] = 0

 

맵 상에서 사과가 있는 곳은 2를 넣었던 것을 기억하자.

만약 뱀 머리가 2가 없는 곳으로 들어가면 늘어난 몸통을 삭제해줘야한다.

 

하지만, 사과가 있는 (2가 있는) 곳으로 가면 늘어난 몸통은 그대로 있게 된다. 

 

    # 방향 전환
    if cmd1 and cmd1[0] == sec:
        # 왼쪽
        if cmd2[0] == "L":
            direction -= 1
            if direction == -1:
                direction = 3
        # 오른쪽
        if cmd2[0] == "D":
            direction += 1
            if direction == 4:
                direction = 0
        # 커맨드 삭제
        cmd1.popleft()
        cmd2.popleft()

그 다음 방향 전환이다.

문제에서 x초가 끝난 뒤, 방향 전환을 수행하고, 방향이 바뀐 곳으로 실제로 이동하는 것은 x+1초이기 때문에 여기서는 방향만 틀어주고 이동은 하지 않겠다.

 

독자들이 스크롤하기 귀찮을 것이니 여기서 방향을 가리키는 코드를 다시 불러오겠다.

# 방향전환
direction = 1
# 위, 오, 밑, 왼
dx = [-1, 0, 1, 0]
dy = [0, 1, 0, -1]

그리고 이동은 다음과 같았다.

   # 움직일 좌표
    # print(snake)
    snake[0][0] = snake[0][0] + dx[direction] # x
    snake[0][1] = snake[0][1] + dy[direction] # y

direction이 1 (뱀은 시작할 때, 오른쪽 방향으로 움직이기 때문에) 이라면 dx = 0, dy = 1 이 되어서 윗쪽으로 움직일 것이다.

만약, (0,1)인데 윗쪽이 아닌가요? 라고 하면, 파이썬에서 x방향은 밑이고, y방향은 오른쪽이라는 것을 기억하기 바란다.

 

커맨드에서 D가 들어온다면, direction은  2가 되고, dx = 1, dy는 0 이 되어서 밑으로 움직이게 된다.

커맨드가 L이었다면 direction은 0이 되고 위로 움직이게 될 것이다.

 

방향 전환을 수행하고 커맨드를 삭제해준다. 

cmd는 선입선출로 빼줘야하기 때문에 deque를 선언했고 popleft를 했다.

 

전체 코드는 다음과 같다.

import sys
from collections import deque 

sys.stdin = open("input.txt", "r")

# 보드의 크기 N
N = int(sys.stdin.readline()) + 2

# 사과의 갯수 K
K = int(sys.stdin.readline())

# 사과 위치 apple
apple = [list(map(int,sys.stdin.readline().split())) for _ in range(K)]
for i in range(len(apple)):
    apple[i] = [apple[i][0], apple[i][1]]

# 사과 = 2
for i in range(len(apple)):
    cage[apple[i][0]][apple[i][1]] = 2

# 변환 횟수 L
L = int(sys.stdin.readline())

# [시간, 방향]
cmd1 = deque()
cmd2 = deque()
for i in range(L):
    cmd1_ipt, cmd2_ipt = sys.stdin.readline().split()
    cmd1.append(cmd1_ipt)
    cmd2.append(cmd2_ipt)
cmd1 = deque(map(int, cmd1))

# 우리
cage = [[0 for col in range(N)] for row in range(N)]


    
# 초
sec = 0

# 뱀
snake = deque()
snake.append([1,1])

# 방향전환
direction = 1
# 위, 오, 밑, 왼
dx = [-1, 0, 1, 0]
dy = [0, 1, 0, -1]


# 커맨드
breaker = False


while True:
    # 시간
    sec += 1
    # 머리 움직임
    snake.appendleft([snake[0][0], snake[0][1]])

    # 움직일 좌표
    snake[0][0] = snake[0][0] + dx[direction] # x
    snake[0][1] = snake[0][1] + dy[direction] # y
    
    # 벽 부딪힘
    if snake[0][0] < 1 or N - 1 <= snake[0][0] or  snake[0][1] < 1 or N - 1 <= snake[0][1]:
        break

    # 몸에 부딪힘
    if len(snake) > 2:
        
        for i in range(2, len(snake)):
            if snake[0] == snake[i]:
                breaker = True
                break
        if breaker == True:
            break
            

    # 사과 먹음
    if cage[snake[0][0]][snake[0][1]] != 2:
        snake.pop()
    else:
        cage[snake[0][0]][snake[0][1]] = 0

    # 방향 전환
    if cmd1 and cmd1[0] == sec:
        # 왼쪽
        if cmd2[0] == "L":
            direction -= 1
            if direction == -1:
                direction = 3
        # 오른쪽
        if cmd2[0] == "D":
            direction += 1
            if direction == 4:
                direction = 0
        # 커맨드 삭제
        cmd1.popleft()
        cmd2.popleft()
    
print(sec)

 

이번 문제는 입력 받는 것과, 입력 받은 것을 어떻게 처리할 지에 대한 것만 있었지, 자신이 생각해서 풀어야 하는 것은 별로 없었던 것 같았다.

 

그리고 간단하고 완벽한 게임이라고는 못하겠지만, 그래도 처음으로 게임을 만들어본 것 같아서 감개무량하다.

 

(이런 간단한 게임조차 이리 품앗이 들다니. 역시 게임은 아무나 만드는 것이 아닌 것 같다. 전세계 게임 개발자 분들에게 경의를 바치면서 이번 포스팅을 종료하겠다.)

 

 

 

 

 

댓글