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

구현 사항은 다음과 같습니다.
- 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()