지뢰찾기 게임

지뢰찾기 게임 제작기 5

잡코신 2024. 4. 2. 18:00
728x90
반응형

지난 시간

지난번엔 지뢰찾기 게임에 로직을 더욱 완성도 높게 수정했다.

이번엔 게임 외적인 부분을 더욱 완성도 높게 수정해 보겠다.

지뢰찾기 게임 만들기

지뢰찾기 룰에 기반해 파이썬으로 제작한다.

 

이번엔 게임 시작과 끝 시작을 기록하고 난이도를 설정할 것이다.

하단에 메뉴가 너무 많이에 상단에 넣어보기로 했다.

time_label = tk.Label(root, text="00:00")
time_label.grid(row=0, column=0,  sticky="w")

difficulty_var = tk.StringVar(root)
difficulty_var.set("어려움")  # 초기 선택: 어려움

difficulty_option_menu = tk.OptionMenu(root, difficulty_var, "쉬움", "보통", "어려움", command=set_difficulty)
difficulty_option_menu.grid(row=0, column=0, columnspan=board_size, sticky="e")

시간을 보여줄 타임 라벨을 설정하고 sticky를 w로 설정해 준다. 그럼 왼쪽(west)정렬이된다.

난이도는 드롭다운으로 설정할 수 있게 해 줄 것이다.

쉬움 보통 어려움으로 넣어주고 sticky를 e로 설정해서 오른(east)정렬을 해줬다.

 

상단 바가 새로 생겼기 때문에 기존 밑에 있던 모든 열이 한 칸 밀려야 한다.

# 게임 보드 생성
board = initialize_board(board_size, num_mines)
revealed = [[' ' for _ in range(board_size)] for _ in range(board_size)]

# 게임 보드의 각 셀을 버튼으로 만들기
buttons = []

for row in range(board_size):
    row_buttons = []
    for col in range(board_size):
        button = tk.Button(root, text='', width=4, height=2)
        button.grid(row=row + 1, column=col)
        row_buttons.append(button)
    buttons.append(row_buttons)


# 게임 종료 메시지 표시
message_label = tk.Label(root, text="", font=("Helvetica", 16))
message_label.grid(row=board_size + 1, columnspan=board_size)

# 다시하기 버튼 추가
restart_button = tk.Button(root, text="다시하기", command=restart_game, font=("Helvetica", 12))
restart_button.grid(row=board_size + 2, column=0, columnspan=board_size)

info_label_frame = tk.Frame(root)
info_label_frame.grid(row=board_size + 3, column=0, columnspan=board_size)

mine_count_label = tk.Label(info_label_frame, text=f"지뢰 개수: {num_mines}", font=("Helvetica", 12))
mine_count_label.grid(row=0, column=0)

remaining_cells_label = tk.Label(info_label_frame, text=f"남은 셀 개수: {remaining_cells}", font=("Helvetica", 12))
remaining_cells_label.grid(row=0, column=1)

row 부분을 모두 +1씩 추가로 더 해줬다.

추가로 실제 row가 들어가는 함수 부분에도 +1을 해줘야 한다.

이때 상단 메뉴를 단 걸 후회하지만 그래도 하단에 메뉴를 늘리는 것보단 결과물이 이쁠 것이다.

 

난이도 설정부터 기능을 만들어보자.

def set_difficulty(selected_difficulty):
    global board_size, num_mines, remaining_cells
    difficulty = selected_difficulty

    for row in range(board_size):
        for col in range(board_size):
            buttons[row][col].destroy() # 버튼 삭제

    if difficulty == "쉬움":
        board_size = 9
        num_mines = 10
    elif difficulty == "보통":
        board_size = 12
        num_mines = 20
    elif difficulty == "어려움":
        board_size = 15
        num_mines = 30
    remaining_cells = board_size * board_size - num_mines
    reset_game()  # 게임 다시 시작

드롭다운에 command로 set_difficulty 함수를 연결할 것이다.

이 함수는 설정된 난이도에 따라 난이도에 따라 보드 사이즈와 지뢰 개수를 조절한다.

난이도를 설정함과 동시에 판이 새로고침 되어야 하기 때문에 보드의 버튼들을 지운다.

def reset_game():
    global board, revealed, remaining_cells, buttons

    board = initialize_board(board_size, num_mines)
    revealed = [[' ' for _ in range(board_size)] for _ in range(board_size)]
    remaining_cells = board_size * board_size - num_mines

    message_label.config(text="")

    buttons = []
    for row in range(board_size):
        row_buttons = []
        for col in range(board_size):
            button = tk.Button(root, text='', width=4, height=2)
            button.grid(row=row + 1, column=col)
            row_buttons.append(button)
        buttons.append(row_buttons)

    for row in range(board_size):
        for col in range(board_size):
            buttons[row][col].bind("<Button-1>", lambda event, row=row, col=col: left_click(event, row, col))
            buttons[row][col].bind("<Button-3>", lambda event, row=row, col=col: right_click(event, row, col))
            
    update_board()
    update_info_labels()

 

보드가 지워진 판을 새로고침하는 함수다.

단순히 위에 처음 게임 세팅하는 코드를 들고 와 복사해 주면 된다.

초기 설정을 어려움으로 한다면 여기까지 복사해도 좋지만 쉬움으로 한다면

판을 업데이트할 때 밑에 하단 메뉴가 겹칠 수 있으므로 하단 메뉴까지 복사하면 좋다.

 

마지막으로 타이머를 달아서 게임 시간을 측정해 줄 것이다.

import time

시간을 측정하기 위해 필요한 새로운 라이브러리이다.

import time

current_time = time.time()
print(current_time)  # 예: 1635933444.7396114
formatted_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(current_time))
print(formatted_time)  # 2023-11-03 10:30:15
time.sleep(2)  # 2초 동안 대기
print("2초 후에 실행됨")

사용법은 간단하다. time을 붙이고 뒤에 원하는 함수를 사용해 주면 된다.

  • time() 함수: 현재 시간을 초 단위로 가져온다. 이 값은 1970년 1월 1일 이후 경과한 시간을 초 단위로 나타낸다.
  • sleep() 함수: 프로그램의 실행을 일정 시간 동안 멈출 수 있다.
  • strftime() 함수: 시간 값을 원하는 형식으로 변환할 수 있다.

우린 이런 시간을 다루는 time 모듈을 이용하여 타이머를 만들 것이다.

# 게임 설정
board_size = 15
num_mines = 30
remaining_cells = board_size * board_size - num_mines
start_time = None

먼저 게임 설정 부분에 start_time이란 변수를 만들어준다.

def left_click(event, row, col):
    global start_time
    if not start_time:
        start_timer()

    if board[row][col] == '*':
        revealed[row][col] = 'X'  # 지뢰 밟음
    else:
        open_empty_cells(board, revealed, row, col)

    update_board()
    update_info_labels()
    check_game_over()

그리고 어떻게 해야 게임의 시작을 정의할 수 있을까 생각해 보았더니 역시 첫 클릭이 제격이라고 판단했다.

click 함수에 start_time이 None이라면(값이 존재하지 않는다면) start_timer() 함수를 실행시키도록 하였다.

def start_timer():
    global start_time
    start_time = time.time()

def update_timer():
    global start_time
    if start_time:
        elapsed_time = time.time() - start_time
        time_label.config(text=f"{int(elapsed_time) // 60:02d}:{int(elapsed_time) % 60:02d}")
    root.after(1000, update_timer)  # 1초마다 경과 시간 업데이트

start_timer 함수에는 time.time()으로 현재 시간(초)을 기록하게 만들었다.

그리고 update_timer를 이용해서 똑같이 이번엔 start_time 값이 존재한다면 현재 시간을 뺀 값을 라벨에 기록했다.

또 1초마다 시간을 업데이트해야 하기에 1000ms가 지나면 자동으로 update_timer함수를 실행해 주는 코드를 넣었다.

update_timer 함수는 mainloop 위에 넣어준다.

def end_timer():
    global start_time
    start_time = None
    root.after_cancel(update_timer)

마지막으로 타이머를 끝내는 함수를 만든다.

start_time을 None으로 되돌리고 update_timer 함수를 실행시키는 것을 취소한다.

타이머가 멈춰야 하는 부분에 해당 함수를 넣어준다.

 

오늘 만들었던 함수가 전부 global이라는 선언을 사용하고 있다.

global 키워드는 파이썬에서 전역 변수를 정의하거나 전역 변수를 함수 내에서 사용할 때 사용된다.

global_var = 10

def func():
    global global_var
    print(global_var) # 10
    global_var = 20
    print(global_var) # 20
    
print(global_var) # 10
func()
print(global_var)  # 20

파이썬에서 함수 내부에서 변수를 정의하면 해당 변수는 기본적으로 지역 변수로 간주된다.

이는 변수가 함수 내에서만 유효하고 함수 외부에서는 액세스 할 수 없음을 의미한다.

그러나 global 키워드를 사용하면 함수 내에서 전역 변수를 참조하거나 수정할 수 있다.

 

지뢰찾기(ver.5) 플레이해 보기

import tkinter as tk
import random
import time

# 게임 설정
board_size = 15
num_mines = 30
remaining_cells = board_size * board_size - num_mines
start_time = None

# 게임 보드 초기화
def initialize_board(size, mines):
    board = [[' ' for _ in range(size)] for _ in range(size)]
    placed_mines = 0
    while placed_mines < mines:
        row, col = random.randint(0, size - 1), random.randint(0, size - 1)
        if board[row][col] != '*':
            board[row][col] = '*'
            placed_mines += 1
    return board

# 지뢰 주변의 지뢰 개수 계산
def count_mines_around(board, row, col):
    count = 0
    for r in range(row - 1, row + 2):
        for c in range(col - 1, col + 2):
            if 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == '*':
                count += 1
    return count

# 주변 빈 칸들을 재귀적으로 열기
def open_empty_cells(board, revealed, row, col):
    if revealed[row][col] != ' ':
        return
    revealed[row][col] = str(count_mines_around(board, row, col))
    global remaining_cells
    remaining_cells -= 1
    if revealed[row][col] == '0':
        for r in range(row - 1, row + 2):
            for c in range(col - 1, col + 2):
                if 0 <= r < len(board) and 0 <= c < len(board[0]):
                    open_empty_cells(board, revealed, r, c)


# 라벨 설정
def update_info_labels():
    mine_count_label.config(text=f"지뢰 개수: {num_mines}")
    remaining_cells_label.config(text=f"남은 셀 개수: {remaining_cells}")

# 다시 시작
def restart_game():
    global board, revealed, remaining_cells

    board = initialize_board(board_size, num_mines)
    revealed = [[' ' for _ in range(board_size)] for _ in range(board_size)]
    remaining_cells = board_size * board_size - num_mines

    message_label.config(text="")

    end_timer()
    update_board()
    update_info_labels()

# 게임 리셋
def reset_game():
    global board, revealed, remaining_cells, buttons

    board = initialize_board(board_size, num_mines)
    revealed = [[' ' for _ in range(board_size)] for _ in range(board_size)]
    remaining_cells = board_size * board_size - num_mines

    message_label.config(text="")

    buttons = []
    for row in range(board_size):
        row_buttons = []
        for col in range(board_size):
            button = tk.Button(root, text='', width=4, height=2)
            button.grid(row=row + 1, column=col)
            row_buttons.append(button)
        buttons.append(row_buttons)

    for row in range(board_size):
        for col in range(board_size):
            buttons[row][col].bind("<Button-1>", lambda event, row=row, col=col: left_click(event, row, col))
            buttons[row][col].bind("<Button-3>", lambda event, row=row, col=col: right_click(event, row, col))


    end_timer()
    update_board()
    update_info_labels()


def set_difficulty(selected_difficulty):
    global board_size, num_mines, remaining_cells
    difficulty = selected_difficulty

    for row in range(board_size):
        for col in range(board_size):
            buttons[row][col].destroy()

    if difficulty == "쉬움":
        board_size = 9
        num_mines = 10
    elif difficulty == "보통":
        board_size = 12
        num_mines = 20
    elif difficulty == "어려움":
        board_size = 15
        num_mines = 30
    remaining_cells = board_size * board_size - num_mines
    reset_game()

# 게임 종료
def check_game_over():
    for row in range(board_size):
        for col in range(board_size):
            if revealed[row][col] == 'X':
                message_label.config(text="지뢰를 밟았습니다! 게임 종료!", fg='red')
                reveal_all_cells()
                end_timer()
                root.update()
                # root.after(2000, root.destroy)  # 2초 후에 게임 종료
                return
    if remaining_cells == 0:
        message_label.config(text="축하합니다! 모든 안전한 셀을 찾았습니다. 게임 승리!")
        reveal_all_cells()
        end_timer()
        # root.update()  # 화면 갱신
        root.after(2000, root.destroy)  # 2초 후에 게임 종료

# 모든 셀을 공개하는 함수
def reveal_all_cells():
    for row in range(board_size):
        for col in range(board_size):
            if board[row][col] == '*':
                revealed[row][col] = 'X'  # 지뢰 셀은 'X'로 공개
            else:
                revealed[row][col] = str(count_mines_around(board, row, col))  # 나머지 셀은 주변 지뢰 개수로 공개

    update_board()

# 게임 실행
def update_board():
    for row in range(board_size):
        for col in range(board_size):
            if revealed[row][col] == ' ':
                buttons[row][col].config(text='', state='normal', bg='light gray')
            else:
                text_to_display = revealed[row][col] if revealed[row][col] != '0' else ''
                buttons[row][col].config(text=text_to_display, state='disabled', bg='white')

def start_timer():
    global start_time
    start_time = time.time()

def end_timer():
    global start_time
    start_time = None
    root.after_cancel(update_timer)
    
def update_timer():
    global start_time
    if start_time:
        elapsed_time = time.time() - start_time
        time_label.config(text=f"{int(elapsed_time) // 60:02d}:{int(elapsed_time) % 60:02d}")
    root.after(1000, update_timer)  # 1초마다 경과 시간 업데이트

def left_click(event, row, col):
    global start_time
    if not start_time:
        start_timer()

    if board[row][col] == '*':
        revealed[row][col] = 'X'
    else:
        open_empty_cells(board, revealed, row, col)

    update_board()
    update_info_labels()
    check_game_over()

def right_click(event, row, col):
    if revealed[row][col] == ' ':
        revealed[row][col] = 'F'  # 깃발 표시
        buttons[row][col].config(text='F', disabledforeground='red') 
    elif revealed[row][col] == 'F':
        revealed[row][col] = ' '  # 깃발 제거
        buttons[row][col].config(text='', disabledforeground='black')

    update_board()

# GUI 초기화
root = tk.Tk()
root.title("지뢰찾기")

time_label = tk.Label(root, text="00:00")
time_label.grid(row=0, column=0,  sticky="w")

difficulty_var = tk.StringVar(root)
difficulty_var.set("어려움")  # 초기 선택: 어려움

difficulty_option_menu = tk.OptionMenu(root, difficulty_var, "쉬움", "보통", "어려움", command=set_difficulty)
difficulty_option_menu.grid(row=0, column=0, columnspan=board_size, sticky="e")

board = initialize_board(board_size, num_mines)
revealed = [[' ' for _ in range(board_size)] for _ in range(board_size)]

buttons = []

for row in range(board_size):
    row_buttons = []
    for col in range(board_size):
        button = tk.Button(root, text='', width=4, height=2)
        button.grid(row=row + 1, column=col)
        row_buttons.append(button)
    buttons.append(row_buttons)

message_label = tk.Label(root, text="", font=("Helvetica", 16))
message_label.grid(row=board_size + 1, columnspan=board_size)

restart_button = tk.Button(root, text="다시하기", command=restart_game, font=("Helvetica", 12))
restart_button.grid(row=board_size + 2, column=0, columnspan=board_size)

info_label_frame = tk.Frame(root)
info_label_frame.grid(row=board_size + 3, column=0, columnspan=board_size)

mine_count_label = tk.Label(info_label_frame, text=f"지뢰 개수: {num_mines}", font=("Helvetica", 12))
mine_count_label.grid(row=0, column=0)

remaining_cells_label = tk.Label(info_label_frame, text=f"남은 셀 개수: {remaining_cells}", font=("Helvetica", 12))
remaining_cells_label.grid(row=0, column=1)

for row in range(board_size):
    for col in range(board_size):
        buttons[row][col].bind("<Button-1>", lambda event, row=row, col=col: left_click(event, row, col))
        buttons[row][col].bind("<Button-3>", lambda event, row=row, col=col: right_click(event, row, col))

update_board()
update_timer()

root.mainloop()

최종 코드이다. 플레이해보자.

아주 잘 작동되는 것을 확인할 수 있었다.

 

오토마우스에 이어 이것도 프로그램으로 만들어볼 것이다.

pyinstaller --onefile --noconsole MineSweeper.py

다음 명령어를 입력해 주고 결과물을 기다린다.

MineSweeper.exe
10.49MB

 

잘 완성된 것을 확인할 수 있었다.

모두 재미있는 게임을 즐겼으면 좋겠다.

728x90
반응형

'지뢰찾기 게임' 카테고리의 다른 글

지뢰찾기 게임 제작기 4  (0) 2024.03.26
지뢰찾기 게임 제작기 3  (0) 2024.03.19
지뢰찾기 게임 제작기 2  (7) 2024.03.12
지뢰찾기 게임 제작기 1  (0) 2024.03.05