파이썬으로 만든 윈도우 명령 프롬프트용 지뢰찾기 게임

파이썬으로 윈도우 명령 프롬프트용 지뢰찾기 게임을 만들어 보았습니다.

윈도우 프롬프트 지뢰찾기

구현 사항은 다음과 같습니다.

  • 4가지 난이도 (쉬움, 중간, 어려움, 사용자 지정)
  • 임시 표시(? 표시)가 가능하도록 구현
  • 지뢰가 있는 칸이 첫 클릭에 열리지 않음

키 조작은 다음과 같습니다.

  • 화살표: 이동
  • Z: 칸 열기 (열리지 않은 칸에만)
  • X: 깃발 표시/해제
  • C: 임시 표시/해제
  • A: 자동으로 인접칸 열기 (숫자칸과 인접한 깃발 수가 같은 경우만)
  • 1-4: 지정된 난이도로 게임 시작
  • R: 게임 재시작
  • ESC: 게임 종료

소스코드는 다음과 같습니다. 내용이 긴 관계로 접어 두었습니다. 클릭하시면 펼쳐집니다.

MineSweeper.py

#!/usr/bin/python3

from random import sample

# Enum
V_OPEN    = 0
V_CLOSED  = 1
V_FLAGGED = 2
V_TEMP    = 3
V_MINE    = 99

Fields = {
    'Mines': [],
    'Open': [],
    'Width': 9,
    'Height': 9,
}

def NullFunction(*args):
    return

CellRefresh = NullFunction

def InitBoard(width, height):
    global Fields
    
    if width <= 0 or height <= 0: return
    tmp_fields = {
        'Mines': [],
        'Open': [],
        'Width': width,
        'Height': height,
    }
    for i in range(height):
        tmp_fields['Mines'].append([0] * width)
        tmp_fields['Open'].append([V_CLOSED] * width)
    Fields = tmp_fields

def SetMines(cnt, excluded = []):
    global Fields
    
    if cnt < 0: cnt = 0
    height = Fields['Height']
    width = Fields['Width']
    field_size = height * width
    # Max mines is 66%
    if cnt > field_size * 2 // 3: cnt = field_size * 2 // 3
    
    excluded_list = []
    for tmp in excluded:
        if not isinstance(tmp, tuple): continue
        if len(tmp) != 2: continue
        excluded_list.append(tmp)
    
    settable_cell = []
    for i in range(width):
        for j in range(height):
            if (i, j) not in excluded_list: settable_cell.append((i, j))
    
    mine_cell = sample(settable_cell, cnt)
    
    for tmp in mine_cell:
        x, y = tmp
        x1 = max(x-1, 0)
        y1 = max(y-1, 0)
        x2 = min(x+1, width-1)
        y2 = min(y+1, height-1)
        for i in range(x1, x2 + 1):
            for j in range(y1, y2 + 1):
                if (i, j) == (x, y):
                    Fields['Mines'][j][i] = V_MINE
                elif Fields['Mines'][j][i] < V_MINE:
                    Fields['Mines'][j][i] += 1

def CellOpen(x, y):
    global Fields

    height = Fields['Height']
    width = Fields['Width']
    
    Fields['Open'][y][x] = V_OPEN
    CellRefresh(x, y)
    
    if Fields['Mines'][y][x] <= 0:
        x1 = max(x-1, 0)
        y1 = max(y-1, 0)
        x2 = min(x+1, width-1)
        y2 = min(y+1, height-1)
        for i in range(x1, x2 + 1):
            for j in range(y1, y2 + 1):
                if Fields['Open'][j][i] == V_CLOSED: CellOpen(i, j)
    
    return Fields['Mines'][y][x]

WinConsole.py

#!/usr/bin/python3

import os
import ctypes
import struct

STD_INPUT_HANDLE   = -10
STD_OUTPUT_HANDLE  = -11
STD_ERROR_HANDLE   = -12

std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
std_err_handle = ctypes.windll.kernel32.GetStdHandle(STD_ERROR_HANDLE)

class CONSOLE_CURSOR_INFO(ctypes.Structure):
    _fields_ = [('dwSize', ctypes.c_int),
                ('bVisible', ctypes.c_byte)]

class COORD(ctypes.Structure):
    pass
 
COORD._fields_ = [("X", ctypes.c_short), ("Y", ctypes.c_short)]

def cls():
    os.system('cls')
    locate(0, 0)

def get_console_info(handle=std_err_handle):
    try:
        csbi = ctypes.create_string_buffer(22)
        res = ctypes.windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi)
        if res:
            return csbi.raw
    except:
        pass

def color(fg, bg, handle=std_out_handle):
    # 0 Black 1 Blue 2 Green 3 Cyan 4 Red 5 Purple 6 Yellow 7 White +8 Bright
    # *16 bg
    if fg < 0 or fg >= 16 or bg < 0 or bg >= 16: return
    bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, bg * 16 + fg)
    return bool

def set_cursor(show=True, handle=std_out_handle):
    cursorInfo = CONSOLE_CURSOR_INFO()
    cursorInfo.dwSize = 1
    cursorInfo.bVisible = 1 if show else 0
    ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(cursorInfo))

def locate(y, x, text=None, handle=std_out_handle):
    ctypes.windll.kernel32.SetConsoleCursorPosition(handle, COORD(x, y))
    if text:
        t = str(text).encode("windows-1252")
        ctypes.windll.kernel32.WriteConsoleA(handle, ctypes.c_char_p(t), len(t), None, None)

def write_console(text='', handle=std_out_handle):
    t = str(text).encode("windows-1252")
    ctypes.windll.kernel32.WriteConsoleA(handle, ctypes.c_char_p(t), len(t), None, None)

def get_terminal_size(handle=std_err_handle):
    con_info = get_console_info(handle)
    if con_info:
        (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", con_info)
        sizex = right - left + 1
        sizey = bottom - top + 1
        return sizex, sizey

def get_location(handle=std_err_handle):
    con_info = get_console_info(handle)
    if con_info:
        (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", con_info)
        return curx, cury

game_windows.py

#!/usr/bin/python3

import WinConsole
from msvcrt import getch

import MineSweeper

MINE_NUMBER_COLOR = [8, 9, 2, 5, 1, 4, 3, 6, 7]
LEVEL_LABEL = [
    ' 1. Easy   ',
    ' 2. Medium ',
    ' 3. Hard   ',
    ' 4. Custom ',
]

Level = 0
Width = 9
Height = 9
Mines = 10
Flags = 0
GameOver = False
Paused = False

def run():
    global Level, Width, Height, Mines, Flags, GameOver, Paused

    CursorX = 0
    CursorY = 0

    FirstMine = True
    
    scr_width, scr_height = WinConsole.get_terminal_size()
    if scr_width < 80 or scr_height < 24:
        print('This game run only 80x24 or larger.')
        return

    MineSweeper.CellRefresh = draw_cell
    
    WinConsole.set_cursor(False)
    
    game_init(Width, Height)
    
    # Key
    while 1:
        c = getch()
        if ord(c) == 27: # ESC
            break
        elif c in [b'1', b'2', b'3', b'4'] or c.lower() == b'r':
            if c in [b'1', b'2', b'3']: Level = int(c) - 1
            elif c == b'4':
                Paused = True
                cust = set_custom()
                Paused = False
                if not cust:
                    draw_level_info()
                    continue
                else:
                    Width, Height, Mines = cust
                    Level = 3
            
            GameOver = False
            FirstMine = True
            CursorX = 0
            CursorY = 0
            set_level()
            Flags = 0
            draw_level()
            game_init(Width, Height)
            
        elif c.lower() == b'z' and not (GameOver or Paused):
            open_flag = False
            if MineSweeper.Fields['Open'][CursorY][CursorX] in [MineSweeper.V_CLOSED, MineSweeper.V_TEMP]:
                if FirstMine:
                    MineSweeper.SetMines(Mines, [(CursorX, CursorY)])
                    FirstMine = False
                open_flag = True
                res = MineSweeper.CellOpen(CursorX, CursorY)
                if res >= MineSweeper.V_MINE:
                    GameOver = True
                    for i in range(Width):
                        for j in range(Height):
                            draw_cell(i, j)
                    print_game_over()
                else:
                    draw_cell(CursorX, CursorY, True)
            
            if open_flag and not GameOver:
                check_victory()

        elif c.lower() == b'a' and not (GameOver or Paused):
            fno = MineSweeper.Fields['Mines'][CursorY][CursorX]
            open_flag = False
            if MineSweeper.Fields['Open'][CursorY][CursorX] == MineSweeper.V_OPEN and fno >= 1:
                x1 = max(CursorX-1, 0)
                y1 = max(CursorY-1, 0)
                x2 = min(CursorX+1, Width-1)
                y2 = min(CursorY+1, Height-1)
                flag_cnt = 0
                for i in range(x1, x2 + 1):
                    for j in range(y1, y2 + 1):
                        if MineSweeper.Fields['Open'][j][i] == MineSweeper.V_FLAGGED: flag_cnt += 1
                if flag_cnt == fno:
                    for i in range(x1, x2 + 1):
                        for j in range(y1, y2 + 1):
                            if MineSweeper.Fields['Open'][j][i] in [MineSweeper.V_CLOSED, MineSweeper.V_TEMP]:
                                open_flag = True
                                res = MineSweeper.CellOpen(i, j)
                                if res >= MineSweeper.V_MINE:
                                    GameOver = True
                                draw_cell(CursorX, CursorY, True)
                if GameOver:
                    for i in range(Width):
                        for j in range(Height):
                            draw_cell(i, j)
                    print_game_over()
            
            if open_flag and not GameOver:
                check_victory()

        elif c.lower() == b'x' and not (GameOver or Paused):
            if MineSweeper.Fields['Open'][CursorY][CursorX] in [MineSweeper.V_CLOSED, MineSweeper.V_TEMP]:
                MineSweeper.Fields['Open'][CursorY][CursorX] = MineSweeper.V_FLAGGED
                Flags += 1
            elif MineSweeper.Fields['Open'][CursorY][CursorX] == MineSweeper.V_FLAGGED:
                MineSweeper.Fields['Open'][CursorY][CursorX] = MineSweeper.V_CLOSED
                Flags -= 1
                
            draw_cell(CursorX, CursorY, True)
            draw_flags()

        elif c.lower() == b'c' and not (GameOver or Paused):
            if MineSweeper.Fields['Open'][CursorY][CursorX] == MineSweeper.V_CLOSED:
                MineSweeper.Fields['Open'][CursorY][CursorX] = MineSweeper.V_TEMP
            elif MineSweeper.Fields['Open'][CursorY][CursorX] == MineSweeper.V_FLAGGED:
                MineSweeper.Fields['Open'][CursorY][CursorX] = MineSweeper.V_TEMP
                Flags -= 1
            elif MineSweeper.Fields['Open'][CursorY][CursorX] == MineSweeper.V_TEMP:
                MineSweeper.Fields['Open'][CursorY][CursorX] = MineSweeper.V_CLOSED
                
            draw_cell(CursorX, CursorY, True)
            draw_flags()
            
        elif ord(c) == 224: # Special Key
            c = getch()
            if ord(c) == 72: # Up
                OldY = CursorY
                if CursorY <= 0: CursorY = Height - 1
                else:            CursorY -= 1
                draw_cell(CursorX, OldY)
                draw_cell(CursorX, CursorY, True)
            elif ord(c) == 75: # Left
                OldX = CursorX
                if CursorX <= 0: CursorX = Width - 1
                else:            CursorX -= 1
                draw_cell(OldX, CursorY)
                draw_cell(CursorX, CursorY, True)
            elif ord(c) == 77: # Right
                OldX = CursorX
                if CursorX >= Width - 1: CursorX = 0
                else:                    CursorX += 1
                draw_cell(OldX, CursorY)
                draw_cell(CursorX, CursorY, True)
            elif ord(c) == 80: # Down
                OldY = CursorY
                if CursorY >= Height - 1: CursorY = 0
                else:                     CursorY += 1
                draw_cell(CursorX, OldY)
                draw_cell(CursorX, CursorY, True)
    
    # End
    WinConsole.set_cursor(True)
    WinConsole.color(15, 0)
    WinConsole.cls()
    WinConsole.write_console("Bye~!\r\n")
    WinConsole.color(7, 0)

def game_init(width, height):
    MineSweeper.InitBoard(width, height)
        
    WinConsole.color(7, 0)
    WinConsole.cls()
    draw_screen()
    
    for i in range(width):
        for j in range(height):
            draw_cell(i, j)
    draw_cell(0, 0, True)
    
def set_level():
    global Width, Height, Mines
    if Level == 0:
        Width = 9
        Height = 9
        Mines = 10
    elif Level == 1:
        Width = 16
        Height = 16
        Mines = 40
    elif Level == 2:
        Width = 30
        Height = 16
        Mines = 99

def draw_screen():
    WinConsole.color(15, 0)
    WinConsole.locate(0, 62, '=' * 17)
    WinConsole.locate(1, 63, '* MineSweeper *')
    WinConsole.locate(2, 62, '=' * 17)
    
    WinConsole.color(7, 0)
    WinConsole.locate(4, 64, 'SELECT LEVEL:')
    
    draw_level()

    WinConsole.color(10, 0)
    WinConsole.locate(15, 64, 'Arrow')
    WinConsole.locate(16, 64, 'Z')
    WinConsole.locate(16, 71, 'X')
    WinConsole.locate(17, 64, 'A')
    WinConsole.locate(17, 71, 'C')
    
    WinConsole.color(11, 0)
    WinConsole.locate(18, 64, '1-4')
    WinConsole.locate(19, 64, 'R')
    WinConsole.locate(20, 64, 'ESC')

    WinConsole.color(15, 0)
    WinConsole.locate(15, 73, 'Move')
    WinConsole.locate(16, 66, 'Open')
    WinConsole.locate(16, 73, 'Flag')
    WinConsole.locate(17, 66, 'Auto')
    WinConsole.locate(17, 73, 'Temp')
    WinConsole.locate(18, 71, 'Levels')
    WinConsole.locate(19, 70, 'Restart')
    WinConsole.locate(20, 73, 'Quit')

    WinConsole.color(13, 0)
    WinConsole.locate(22, 62, 'Created by  PJW48')
    
def draw_level():
    for i in range(len(LEVEL_LABEL)):
        if i == Level:
            WinConsole.color(15, 1)
        else:
            WinConsole.color(7, 0)
        WinConsole.locate(5+i, 65, LEVEL_LABEL[i])
        
    WinConsole.color(15, 0)
    WinConsole.locate(10, 65, 'Width')
    WinConsole.locate(11, 65, 'Height')
    WinConsole.locate(12, 65, 'Mines')
    WinConsole.locate(13, 65, 'Flags')
    
    draw_level_info()
    
    draw_flags()

def draw_level_info(cur = None, w = None, h = None, m = None):
    if not w: w = Width
    if not h: h = Height
    if not m: m = Mines
    WinConsole.color(9, 0)
    if cur != 0: WinConsole.locate(10, 74, '%3d' % w)
    if cur != 1: WinConsole.locate(11, 74, '%3d' % h)
    WinConsole.color(12, 0)
    if cur != 2: WinConsole.locate(12, 74, '%3d' % m)

    
def draw_flags():
    if Flags == Mines:
        WinConsole.color(10, 0)
    elif Flags > Mines:
        WinConsole.color(13, 0)
    else:
        WinConsole.color(14, 0)
    WinConsole.locate(13, 74, '%3d' % Flags)

def draw_cell(x, y, cursor = False):
    WinConsole.locate(y, x * 2 + 1)
    if MineSweeper.Fields['Open'][y][x] in [MineSweeper.V_CLOSED, MineSweeper.V_TEMP]:
        if GameOver and MineSweeper.Fields['Mines'][y][x] >= MineSweeper.V_MINE:
            WinConsole.color(12, 0)
            WinConsole.write_console("@")
        else:
            if cursor and not GameOver:
                WinConsole.color(0, 7)
            else:
                WinConsole.color(8, 0)
            if MineSweeper.Fields['Open'][y][x] == MineSweeper.V_TEMP:
                WinConsole.write_console("?")
            else:
                WinConsole.write_console("#")
    elif MineSweeper.Fields['Open'][y][x] == MineSweeper.V_FLAGGED:
        if GameOver and MineSweeper.Fields['Mines'][y][x] < MineSweeper.V_MINE:
            WinConsole.color(14, 0)
            WinConsole.write_console("x")
        else:
            if cursor and not GameOver:
                WinConsole.color(0, 7)
            else:
                WinConsole.color(15, 0)
            WinConsole.write_console("F")
    else:
        m = MineSweeper.Fields['Mines'][y][x]
        if m <= 8:
            if cursor and not GameOver:
                WinConsole.color(0, 7)
            else:
                WinConsole.color(MINE_NUMBER_COLOR[m], 0)
            if m == 0:
                WinConsole.write_console(".")
            else:
                WinConsole.write_console(m)
        elif m >= MineSweeper.V_MINE:
            WinConsole.color(15, 4)
            WinConsole.write_console("@")

def check_victory():
    global Flags, GameOver
    closed_cnt = 0
    for i in range(Width):
        for j in range(Height):
            if MineSweeper.Fields['Open'][j][i] in [MineSweeper.V_CLOSED, MineSweeper.V_TEMP]:
                closed_cnt += 1
    if Mines == Flags + closed_cnt:
        GameOver = True
        if closed_cnt > 0:
            for i in range(Width):
                for j in range(Height):
                    if MineSweeper.Fields['Open'][j][i] in [MineSweeper.V_CLOSED, MineSweeper.V_TEMP]:
                        MineSweeper.Fields['Open'][j][i] = MineSweeper.V_FLAGGED
                        Flags += 1
                        draw_cell(i, j)
            draw_flags()
        print_game_over(True)

def print_game_over(victory = False):
    WinConsole.color(7, 0)
    WinConsole.locate(17, 64, ' ' * 13)
    if victory:
        WinConsole.color(10, 0)
        WinConsole.locate(15, 64, '  Congrats!  ')
        WinConsole.locate(16, 64, ' YOU WIN! :) ')
    else:
        WinConsole.color(12, 0)
        WinConsole.locate(15, 64, '  GAME OVER  ')
        WinConsole.locate(16, 64, 'Try Again ...')
        
def set_custom():
    cur = 0
    ok = False
    brk = False
    adjust = False
    tmp_width = str(Width)
    tmp_height = str(Height)
    tmp_mines = str(Mines)
    WinConsole.set_cursor(True)
    # Key
    while 1:
        t_width = int("0%s" % tmp_width)
        t_height = int("0%s" % tmp_height)
        t_mines = int("0%s" % tmp_mines)
        draw_level_info(cur, t_width, t_height, t_mines)
        WinConsole.color(15, 1)
        WinConsole.locate(10 + cur, 74, '   ')
        if cur == 0:
            WinConsole.locate(10, 74, tmp_width)
        elif cur == 1:
            WinConsole.locate(11, 74, tmp_height)
        elif cur == 2:
            WinConsole.locate(12, 74, tmp_mines)
        c = getch()
        if ord(c) == 27: # ESC
            brk = True
        elif ord(c) == 13: # Enter
            ok = True
        elif ord(c) >= 48 and ord(c) <= 57: # Num
            if cur == 0 and len(tmp_width) < 2: tmp_width += c.decode()
            elif cur == 1 and len(tmp_height) < 2: tmp_height += c.decode()
            elif cur == 2 and len(tmp_mines) < 3: tmp_mines += c.decode()
        elif ord(c) == 8: # Bksp
            if cur == 0 and len(tmp_width) > 0: tmp_width = tmp_width[:-1]
            elif cur == 1 and len(tmp_height) > 0: tmp_height = tmp_height[:-1]
            elif cur == 2 and len(tmp_mines) > 0: tmp_mines = tmp_mines[:-1]
        elif ord(c) == 224: # Special Key
            c = getch()
            if ord(c) == 72: # Up
                if cur <= 0: cur = 2
                else:        cur -= 1
                adjust = True
            elif ord(c) == 80: # Down
                if cur >= 2: cur = 0
                else:        cur += 1
                adjust = True

        if ok or brk or adjust:
            if t_width < 9: tmp_width = '9'
            elif t_width > 30: tmp_width = '30'
            if t_height < 9: tmp_height = '9'
            elif t_height > 24: tmp_height = '24'
            if t_mines > t_width * t_height * 6 // 10: tmp_mines = str(t_width * t_height * 6 // 10)
            adjust = False
        
        if ok or brk: break

    WinConsole.set_cursor(False)
    if ok:
        return int(tmp_width), int(tmp_height), int(tmp_mines)
    else:
        return

main.py

#!/usr/bin/python3

import os

if __name__ == '__main__':
    if os.name == 'nt':
        import game_windows
        game_windows.run()

다운로드: https://pjw48.net/files/minesweeper_win_prompt_py.zip

답글 남기기

이메일 주소는 공개되지 않습니다.