파이썬으로 윈도우 명령 프롬프트용 지뢰찾기 게임을 만들어 보았습니다.
구현 사항은 다음과 같습니다.
- 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()