개발로 사고하기

🦖 미니 공룡 게임으로 배우는 방향 벡터 완성

tues 2025. 8. 19. 17:19

 

⬇️게임 실행하기⬇️

https://disosaur-game.onrender.com/

 

🦖미니 공룡게임🦖

🦖미니 공룡게임🦖 ➡️ 앞으로 이동 🔄 게임 초기화

disosaur-game.onrender.com

 

https://tuesday04.tistory.com/87

 

방향 전환 수학 이해를 위한 미니 게임

https://github.com/veryyounng/disosaur_game GitHub - veryyounng/disosaur_gameContribute to veryyounng/disosaur_game development by creating an account on GitHub.github.com 💡 제작 계기백준 알고리즘 문제 3190번 - 뱀을 풀던 중,뱀이 방

tuesday04.tistory.com

 

https://github.com/veryyounng/disosaur_game.git

 

GitHub - veryyounng/disosaur_game

Contribute to veryyounng/disosaur_game development by creating an account on GitHub.

github.com

 

1) 게임 규칙 요약

  • 보드는 5×5, 공룡은 (0,0) 에서 시작 (머리 1칸)
  • 방향 벡터로 이동: right=(1,0), left=(-1,0), up=(0,-1), down=(0,1)
  • 은 시작 시 랜덤 배치
  • 사과(🍎) 를 먹으면 길이 +1 (꼬리를 지우지 않음)
  • 반대 방향(180°) 즉시 전환은 금지(길이 ≥ 2일 때) → 즉시 자기충돌 방지
  • 집(🏠) = (4,4) 에 도달하면 승리: “🏁 공룡이 무사히 도착했어요!” 표시 후 입력 막음
  • 충돌(벽/경계/자기몸)이면 게임 오버

 

< 구현 화면 >

 

 

방향키 조절하는 법 

위, 아래, 왼쪽, 오른쪽 방향키를 클릭하면 방향이 전환된다.

원하는 방향으로 이동한다.

 

 

 

 

충돌

벽/경계/자기몸과 닿으면 충돌 !

게임 오버

 

사과(🍎) 를 먹으면 길이 +1

 

 

 

반대 방향(180°) 즉시 전환은 금지

 

 

 

집(🏠)

 

집에 무사히 도착하면 게임 승리!

 

 

2) 핵심 설계: “다음 위치 = 현재 위치 + 방향 벡터”

모든 움직임은 2D 벡터 합으로 표현한다.

dx, dy = DIRECTION
nx, ny = head_x + dx, head_y + dy
  • 이동: 새 머리를 앞에 추가하고(리스트 맨 앞),
    • 사과를 먹지 않으면 꼬리를 하나 제거(길이 유지)
    • 사과를 먹으면 꼬리를 제거하지 않음(=성장)

이 간단한 규칙만으로 방향 전환 시 몸이 자연스럽게 꺾여 따라오는 효과가 생긴다.

 

3) 핵심 코드

 

전역 상태

# constants
BOARD_SIZE = 5
N_WALLS = 3
GOAL = (4, 4)          # 집 좌표
GAME_OVER = False

DIR_MAP = {
    "up": (0, -1), "down": (0, 1), "left": (-1, 0), "right": (1, 0)
}
DIRECTION = DIR_MAP["right"]

SNAKE = [[1, 1]]       # 머리만 1칸 시작
WALLS = []             # [(x,y), ...]
APPLE = None           # (x,y) or None

def in_bounds(x, y): return 0 <= x < BOARD_SIZE and 0 <= y < BOARD_SIZE

def random_empty_cell(excludes:set[tuple[int,int]]):
    cand = [(x,y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE)
            if (x,y) not in excludes]
    return random.choice(cand) if cand else None

def opposite(a, b):  # a가 b와 정반대?
    return a[0] == -b[0] and a[1] == -b[1]

 

 

초기화 & 랜덤 배치

def reset_game():
    global SNAKE, WALLS, DIRECTION, APPLE, GAME_OVER
    DIRECTION = DIR_MAP["right"]
    SNAKE = [[1, 1]]
    WALLS = []
    GAME_OVER = False

    occupied = {(x, y) for (x, y) in SNAKE}
    occupied.add(GOAL)                 # 집에는 아무것도 두지 않음

    # 랜덤 벽
    while len(WALLS) < N_WALLS:
        x, y = random.randint(0,4), random.randint(0,4)
        if (x,y) not in occupied and (x,y) not in WALLS:
            WALLS.append((x,y))
            occupied.add((x,y))

    # 사과
    APPLE = random_empty_cell(occupied)

 

 

상태 조회

@app.get("/api/state")
def get_state():
    board = [["" for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]

    for (x,y) in WALLS: board[y][x] = "🧱"
    gx, gy = GOAL;      board[gy][gx] = "🏠"
    if APPLE:           board[APPLE[1]][APPLE[0]] = "🍎"

    for i, (x,y) in enumerate(SNAKE):
        board[y][x] = "🦖" if i==0 else "🦕"

    return dict(board=board, snake=SNAKE, apple=APPLE, walls=WALLS,
                direction=DIRECTION, goal=GOAL, game_over=GAME_OVER)

 

 

방향 전환: 반대방향 금지(길이≥2)

class DirectionRequest(BaseModel):
    direction: str

@app.post("/api/change_direction")
def change_direction(req: DirectionRequest):
    global DIRECTION, GAME_OVER
    if GAME_OVER: return {"status": "finished"}
    d = req.direction.lower()
    if d not in DIR_MAP: return {"status": "invalid"}

    new_dir = DIR_MAP[d]
    # 핵심: 길이가 2칸 이상이면 즉시 반대방향 금지 (자기충돌 방지)
    if len(SNAKE) >= 2 and opposite(new_dir, DIRECTION):
        return {"status": "invalid", "reason": "opposite turn not allowed"}

    DIRECTION = new_dir
    return {"status": "ok", "direction": DIRECTION}

 

 

이동: 성장/충돌/목표 도달

@app.post("/api/move")
def move():
    global SNAKE, APPLE, GAME_OVER
    if GAME_OVER: return {"status": "finished", "game_over": True}

    dx, dy = DIRECTION
    head_x, head_y = SNAKE[0]
    nx, ny = head_x + dx, head_y + dy

    # 1) 경계/벽 충돌
    if not in_bounds(nx, ny) or (nx, ny) in WALLS:
        GAME_OVER = True
        return {"status": "crash", "game_over": True}

    # 2) 목표 도달 (집)
    if (nx, ny) == GOAL:
        SNAKE = [[nx, ny]] + SNAKE[:-1] if len(SNAKE) > 0 else [[nx, ny]]
        GAME_OVER = True
        return {"status": "goal", "game_over": True, "snake": SNAKE}

    will_grow = (APPLE is not None and (nx, ny) == APPLE)

    # 3) 자기충돌 (단, 성장하지 않고 '꼬리 자리'로 이동하는 건 허용)
    body_set = set(map(tuple, SNAKE))
    tail = tuple(SNAKE[-1])
    if (nx, ny) in body_set and not (not will_grow and (nx, ny) == tail):
        GAME_OVER = True
        return {"status": "crash", "game_over": True}

    # 4) 이동/성장
    new_head = [nx, ny]
    if will_grow:
        SNAKE = [new_head] + SNAKE               # 꼬리 유지 → 성장(+1)
        occupied = set(map(tuple, SNAKE)).union(WALLS, {GOAL})
        APPLE = random_empty_cell(occupied)      # 새 사과
        return {"status": "ok", "event": "eat", "snake": SNAKE, "apple": APPLE}
    else:
        SNAKE = [new_head] + SNAKE[:-1]          # 꼬리 제거 → 길이 유지
        return {"status": "ok", "event": "move", "snake": SNAKE, "apple": APPLE}

 

4) “반대방향 금지”는 왜 필요할까?

길이가 2칸 이상일 때 즉시 U턴을 허용하면, 다음 이동이 곧바로 자기 몸(바로 뒤 segment) 으로 들어가 즉시 게임오버가 난다.
그래서 전통적인 스네이크 룰은 즉시 반대 전환 금지를 채택한다.

필요하면 변형 룰도 가능:
(A) 완전 허용(자살 가능), (B) ‘이번 턴 꼬리로 돌아가기’만 허용, (C) 즉시 몸을 뒤집어 Head↔Tail 스왑 등.

 

 

5) 배운 점

이번 공룡게임 구현을 통해 방향 벡터, 예외 처리(반대 방향 이동 방지), 큐/리스트를 활용한 자료구조 관리 같은 알고리즘 개념이 얼마나 중요한지 체감했습니다.
또한 조건 분기(사과 먹기, 충돌 판정, 도착 여부)를 통해 게임 규칙을 코드로 옮기는 과정에서 문제 해결력을 키울 수 있었습니다.