diff options
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | logic.py | 391 | ||||
| -rw-r--r-- | ouija.py | 327 | ||||
| -rwxr-xr-x | sweepy.py | 243 |
4 files changed, 972 insertions, 1 deletions
@@ -1,3 +1,13 @@ # sweepy -minesweeper in python with curses
\ No newline at end of file +minesweeper in python with curses + +## features + - save/resume game + - 2 size boards + - ~x~ pReTtYcOlOrS ~x~ + +## coming soon? + - custom boards + - local multiplayer + - scoring? diff --git a/logic.py b/logic.py new file mode 100644 index 0000000..01956ee --- /dev/null +++ b/logic.py @@ -0,0 +1,391 @@ +import random + +# 8x8_10 +# 16x16_40 +# 16x30_99 + +# under variables +UNDER_DEFAULT = 0 +UNDER_BOMB = -1 + +# over variables +OVER_DEFAULT = '_' +OVER_FLAGGED = 'x' +OVER_UNCOVERED = 'u' +OVER_MOVES = [OVER_FLAGGED, OVER_UNCOVERED] + +# board variables +BOARD_COVERED = '_' +BOARD_FLAGGED = '!' +BOARD_BOMB_UNCOVERED = 'x' +BOARD_BOMB_COVERED = '-' +BOARD_BOMB_FLAGGED = '+' + +DELIMITER = '|' + +# get a random seed value +def get_seed(): + return random.randint(1,1000000) + +class MinesweeperLogic: + + # field is the bombs and the values + # board is uncovered/flagged + UNDER = [] + OVER = [] + + # other variables + WIDTH = 0 + HEIGHT = 0 + BOMBS = 0 + SEED = 0 + MOVES = [] + + def do_all_moves(self): + for move in self.MOVES: + self.do_move(int(move[0]), int(move[1]), move[2]) + + def load(self, filename): + # get a handle to the global variables + #global WIDTH, HEIGHT, BOMBS, SEED, MOVES + + # grab the lines from the file + with open(filename, 'r') as fil: + istr = fil.readline().strip() + moves = fil.readlines() + + # the first line must have 4 numeric values + try: + ilist = [int(i) for i in istr.split('|')] + except ValueError as e: + return False + if len(ilist) == 4: + + # set up global variables + self.WIDTH = ilist[0] + self.HEIGHT = ilist[1] + self.BOMBS = ilist[2] + self.SEED = ilist[3] + + self.setup() + + # validate moves + for m in moves: + vm = self.validate_move(m.strip()) + if not vm: + return False + #self.MOVES.append(vm) + self.do_move(vm[0], vm[1], vm[2]) + + # if all the moves are valid, do them + #self.do_all_moves() + #for move in self.MOVES: + #self.do_move(int(move[0]), int(move[1]), move[2]) + + return True + return False + + def save(self, filename=None): + # set a filename, if missing + if filename is None: + filename = "{}.sweepy".format(str(self.SEED)) + + with open(filename, "w") as fn: + + # write the header information + fn.write("{}|{}|{}|{}\n".format(str(self.WIDTH), + str(self.HEIGHT), + str(self.BOMBS), + str(self.SEED))) + + # write the moves + for move in self.MOVES: + fn.write("{}\n".format(str(move))) + + def new_game(self, width=8, height=8, bombs=10): + self.WIDTH = width + self.HEIGHT = height + self.BOMBS = bombs + self.setup() + self.do_first_move() + + # handle interactive highlighting + def get_first_cell(self): + for y, col in enumerate(self.OVER): + for x, cell in enumerate(col): + if cell == OVER_DEFAULT: + return y, x + + def get_covered_cells(self): + out = [] + for y, col in enumerate(self.OVER): + for x, cell in enumerate(col): + if cell == OVER_DEFAULT: + out.append([y,x]) + return out + + def closest(self, y, x): + covered_cells = self.get_covered_cells() + for point in covered_cells: + if point[0] == y and point[1] > x: + return point[0], point[1] + elif point[0] > y: + return point[0], point[1] + return covered_cells[0][0], covered_cells[0][1] + + + def get_closest_cell(self, y, x): + # set some defaults that will be overwritten + min_off = self.WIDTH + self.HEIGHT + ny = -1 + nx = -1 + + # go through the directions + ry, rx, ro = self.get_right(y, x) + if 0 < ro < min_off: + min_off = ro + ny = ry + nx = rx + + dy, dx, do = self.get_down(y, x) + if 0 < do < min_off: + min_off = do + ny = dy + nx = dx + + ly, lx, lo = self.get_left(y, x) + if 0 < lo < min_off: + min_off = lo + ny = ly + nx = lx + + uy, ux, uo = self.get_up(y, x) + if 0 < uo < min_off: + min_off = uo + ny = uy + nx = ux + + # if we found something, return it + if min_off < self.WIDTH + self.HEIGHT: + return ny, nx + else: + return self.get_first_cell() + + def get_left(self, y, x): + col = self.OVER[y] + for ix in range(1,x+1): + adj_x = x - ix + if col[adj_x] in (OVER_DEFAULT, OVER_FLAGGED): + return y, adj_x, abs(adj_x - x) + return y, x, 0 + + def get_right(self, y, x): + col = self.OVER[y] + for ix in range(x+1,len(col)): + if col[ix] in (OVER_DEFAULT, OVER_FLAGGED): + return y, ix, abs(ix - x) + return y, x, 0 + + def get_up(self, y, x): + row = [self.OVER[i][x] for i in range(len(self.OVER))] + for iy in range(1,y+1): + adj_y = y - iy + if row[adj_y] in (OVER_DEFAULT, OVER_FLAGGED): + return adj_y, x, abs(adj_y - y) + return y, x, 0 + + def get_down(self, y, x): + row = [self.OVER[i][x] for i in range(len(self.OVER))] + for iy in range(y+1, len(row)): + if row[iy] in (OVER_DEFAULT, OVER_FLAGGED): + return iy, x, abs(iy - y) + return y, x, 0 + + def do_first_move(self): + for y, col in enumerate(self.UNDER): + for x, cell in enumerate(self.UNDER[y]): + if cell == UNDER_DEFAULT: + self.do_move(y, x, OVER_UNCOVERED) + return + + def validate_move(self, move): + + if type(move) is list: + mlist = move + else: + mlist = move.split(DELIMITER) + + try: + if not 0 <= int(mlist[0]) < self.WIDTH: + return None + mlist[0] = int(mlist[0]) + + if not 0 <= int(mlist[1]) < self.HEIGHT: + return None + mlist[1] = int(mlist[1]) + + if mlist[2] in OVER_MOVES: + return mlist + except ValueError as e: + return None + + # returns true if move is successful/valid + # returns false otherwise + def do_move(self, col, row, move, propagated=False): + + # if the cell hasn't been uncovered, try to uncover it + if self.OVER[col][row] == OVER_DEFAULT: + if move == OVER_FLAGGED: + if not propagated: + self.MOVES.append("{}|{}|{}".format(str(col), str(row), move)) + self.OVER[col][row] = move + elif move == OVER_UNCOVERED: + + # uncover the targeted cell + if not propagated: + self.MOVES.append("{}|{}|{}".format(str(col), str(row), move)) + self.OVER[col][row] = move + + # if the uncovered cell is the default, + # uncover neighboring cells + if self.UNDER[col][row] == UNDER_DEFAULT: + neighbors = self.get_valid_neighbors(col, row) + for n in neighbors: + n.extend(move) # add the move to the neighbor + vm = self.validate_move(n) + if vm and self.UNDER[vm[0]][vm[1]] != UNDER_BOMB: + self.do_move(vm[0], vm[1], vm[2], True) + elif self.OVER[col][row] == OVER_FLAGGED and move == OVER_FLAGGED: + if not propagated: + self.MOVES.append("{}|{}|{}".format(str(col), str(row), move)) + self.OVER[col][row] = OVER_DEFAULT + else: + return False + + def setup(self): + + # grab a handle to the global variables + if self.SEED == 0: + self.SEED = get_seed() + + # initialize with the width + # use None objects, as these will be ultimately be lists + self.UNDER = [None] * self.WIDTH + self.OVER = [None] * self.WIDTH + + # add the height rows (use the defaults) + for i in range(self.WIDTH): + under_row = [UNDER_DEFAULT] * self.HEIGHT + self.UNDER[i] = under_row + + over_row = [OVER_DEFAULT] * self.HEIGHT + self.OVER[i] = over_row + + # generate and place the bombs + random.seed(self.SEED) + for i in range(self.BOMBS): + while True: + w = random.randint(0, self.WIDTH - 1) + h = random.randint(0, self.HEIGHT - 1) + if self.UNDER[w][h] == UNDER_DEFAULT: + self.UNDER[w][h] = UNDER_BOMB + break + + # calculate the values next to the bombs + for w in range(self.WIDTH): + for h in range(self.HEIGHT): + if self.UNDER[w][h] == UNDER_BOMB: + neighbors = self.get_valid_neighbors(w, h) + for n in neighbors: + if self.UNDER[n[0]][n[1]] != UNDER_BOMB: + self.UNDER[n[0]][n[1]] += 1 + + def get_valid_neighbors(self, col, row): + + # calculate the values + col_left = col - 1 + col_right = col + 1 + row_up = row - 1 + row_down = row + 1 + + # calculate which directions we can go + left = True if col_left >= 0 else False + right = True if col_right < self.WIDTH else False + up = True if row_up >= 0 else False + down = True if row_down < self.HEIGHT else False + + # valid neighbors + vns = [] + if left and up: + vns.append([col_left, row_up]) + if left: + vns.append([col_left, row]) + if left and down: + vns.append([col_left, row_down]) + if right and up: + vns.append([col_right, row_up]) + if right: + vns.append([col_right, row]) + if right and down: + vns.append([col_right, row_down]) + if up: + vns.append([col, row_up]) + if down: + vns.append([col, row_down]) + + return vns + + def has_won(self): + # win condition: all non-bombs are uncovered + for w in range(0,self.WIDTH): + for h in range(0,self.HEIGHT): + if self.UNDER[w][h] != UNDER_BOMB and self.OVER[w][h] != OVER_UNCOVERED: + return False + return True + + def has_lost(self): + # lose condition: at least one bomb is uncovered + for w in range(0,self.WIDTH): + for h in range(0,self.HEIGHT): + if self.UNDER[w][h] == UNDER_BOMB and self.OVER[w][h] == OVER_UNCOVERED: + return True + return False + + def get_board(self): + # this will be what the user sees + + lost = self.has_lost() + + out = [] + for c in range(self.WIDTH): + col = self.OVER[c] + inner = [] + for r in range(self.HEIGHT): + row = col[r] + + under_cell = self.UNDER[c][r] + + if row == OVER_UNCOVERED and under_cell >= 0: + inner.append(str(under_cell)) + elif row == OVER_UNCOVERED: + inner.append(BOARD_BOMB_UNCOVERED) + elif row == OVER_FLAGGED: + if lost and under_cell == UNDER_BOMB: + inner.append(BOARD_BOMB_FLAGGED) + else: + inner.append(BOARD_FLAGGED) + else: + if lost and under_cell == UNDER_BOMB: + inner.append(BOARD_BOMB_COVERED) + else: + inner.append(BOARD_COVERED) + out.append(inner) + return out + + def get_flag_count(self): + total = 0 + for col in self.OVER: + for cell in col: + if cell == OVER_FLAGGED: + total += 1 + return total diff --git a/ouija.py b/ouija.py new file mode 100644 index 0000000..4dc56b1 --- /dev/null +++ b/ouija.py @@ -0,0 +1,327 @@ +import curses +from functools import reduce +from enum import Enum +import math + +class Ouija: + + class Align(Enum): + LEFT = 0 + RIGHT = 1 + CENTER = 2 + + DEFAULT_VERT_EDGE = "|" + DEFAULT_HORIZ_EDGE = "~" + DEFAULT_CORNER = ":" + DEFAULT_HORIZ_PAD = 1 + DEFAULT_VERT_PAD = 0 + DEFAULT_HORIZ_MARGIN = 0 + DEFAULT_VERT_MARGIN = 0 + DEFAULT_UNIFORM_WIDTH = True + DEFAULT_ALIGN = Align.LEFT + + class _OuijaColor: + + DEFAULT = -1 + BLACK = 0 + RED = 1 + GREEN = 2 + BLUE = 4 + CYAN = 6 + WHITE = 7 + ORANGE = 9 + YELLOW = 11 + + def __init__(self, fg=DEFAULT, bg=DEFAULT): + self.fg = fg + self.bg = bg + + def color_pair(self): + # start with an offset of 1 (can't do negatives) + out = 1 + + # hacky bitwise operator for different fg/bg colors + white_bg = 16 + white_fg = 32 + black_fg = 64 + + if self.bg == self.DEFAULT: + return out + self.fg + elif self.bg == self.WHITE: + return out + white_bg + self.fg + elif self.fg == self.WHITE: + return out + white_fg + self.bg + else: + return out + black_fg + self.bg + + COLORS = { + "DEFAULT" : _OuijaColor(), + "BLACK" : _OuijaColor(_OuijaColor.BLACK), + "RED" : _OuijaColor(_OuijaColor.RED), + "GREEN" : _OuijaColor(_OuijaColor.GREEN), + "BLUE" : _OuijaColor(_OuijaColor.BLUE), + "CYAN" : _OuijaColor(_OuijaColor.CYAN), + "WHITE" : _OuijaColor(_OuijaColor.WHITE), + "ORANGE" : _OuijaColor(_OuijaColor.ORANGE), + "YELLOW" : _OuijaColor(_OuijaColor.YELLOW), + + "RED_ON_WHITE" : _OuijaColor(_OuijaColor.RED, _OuijaColor.WHITE), + "GREEN_ON_WHITE" : _OuijaColor(_OuijaColor.GREEN, _OuijaColor.WHITE), + "BLUE_ON_WHITE" : _OuijaColor(_OuijaColor.BLUE, _OuijaColor.WHITE), + "CYAN_ON_WHITE" : _OuijaColor(_OuijaColor.CYAN, _OuijaColor.WHITE), + "ORANGE_ON_WHITE" : _OuijaColor(_OuijaColor.ORANGE, _OuijaColor.WHITE), + "YELLOW_ON_WHITE" : _OuijaColor(_OuijaColor.YELLOW, _OuijaColor.WHITE), + + "WHITE_ON_BLACK" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.BLACK), + "WHITE_ON_RED" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.RED), + "WHITE_ON_GREEN" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.GREEN), + "WHITE_ON_BLUE" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.BLUE), + "WHITE_ON_CYAN" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.CYAN), + "WHITE_ON_ORANGE" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.ORANGE), + "WHITE_ON_YELLOW" : _OuijaColor(_OuijaColor.WHITE, _OuijaColor.YELLOW), + + "BLACK_ON_WHITE" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.WHITE), + "BLACK_ON_RED" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.RED), + "BLACK_ON_GREEN" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.GREEN), + "BLACK_ON_BLUE" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.BLUE), + "BLACK_ON_CYAN" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.CYAN), + "BLACK_ON_ORANGE" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.ORANGE), + "BLACK_ON_YELLOW" : _OuijaColor(_OuijaColor.BLACK, _OuijaColor.YELLOW), + } + + def _setup_curses_colors(): + + # initialize colors for curses + curses.start_color() + curses.use_default_colors() + + for c in Ouija.COLORS.keys(): + color = Ouija.COLORS[c] + curses.init_pair(color.color_pair(), color.fg, color.bg) + + def __init__(self, stdscr, y, x): + self.stdscr = stdscr + self.y = y + self.x = x + + # setup default styles + self.style() + + # setup the curses colors + Ouija._setup_curses_colors() + + # helper method to access style variables + def style(self, + vert_edge=DEFAULT_VERT_EDGE, + horiz_edge=DEFAULT_HORIZ_EDGE, + corner=DEFAULT_CORNER, + horiz_pad=DEFAULT_HORIZ_PAD, + vert_pad=DEFAULT_VERT_PAD, + horiz_margin=DEFAULT_HORIZ_MARGIN, + vert_margin=DEFAULT_VERT_MARGIN, + uniform_width=DEFAULT_UNIFORM_WIDTH, + align=DEFAULT_ALIGN): + self.vert_edge = vert_edge + self.horiz_edge = horiz_edge + self.corner = corner + self.horiz_pad = horiz_pad + self.vert_pad = vert_pad + self.horiz_margin = horiz_margin + self.vert_margin = vert_margin + self.uniform_width = uniform_width + self.align = align + + # setup the tiles + def setup_tiles(self, ts): + if type(ts) != list: + return False + self.tiles = {} + for t in ts: + if type(t) != Tile: + return False + self.tiles[t.in_val] = t + print(str(self.tiles)) + return True + + def calc_cell_width(self, cell_value): + edges = 2 if self.vert_edge is not None else 0 + return len(cell_value) + (2 * self.horiz_pad) + edges + + def calc_row_width(self, row): + width = 0 + for cell in row: + + # if there's a cooresponding display value, use its length + # if not, just take the width directly + if cell in self.tiles: + width += self.calc_cell_width(self.tiles[cell].out_val) + else: + width += self.calc_cell_width(cell) + + # if there's no margin, we double counted some dividers + if self.horiz_margin == 0: + width -= len(row) - 1 + elif self.horiz_margin > 1: + width += (self.horiz_margin - 1) * (len(row) - 1) + + return width + + # draw the board, optionally highlighting a cell + def draw_board(self, board, hy=-1, hx=-1): + + # calculate the longest row + max_row_length = max([len(row) for row in board]) + + if self.uniform_width: + # get the widest possible cell + out_vals = [i.out_val for i in self.tiles.values()] + max_cell_width = max([len(x) for x in out_vals]) + + # the max row width is the longest row of the widest cells + max_row_width = self.calc_row_width(["." * max_cell_width] * max_row_length) + else: + max_row_width = max([self.calc_row_width(row) for row in board]) + + # if specified, make every key as wide as the widest key + if self.uniform_width: + out_vals = [i.out_val for i in self.tiles.values()] + max_width = max([len(x) for x in out_vals]) + + # keep track of y-coordinate, and board y-index + cur_y = self.y + iy = 0 + + # loop through all columns in board + for col in board: + + if self.uniform_width: + cur_row_width = self.calc_row_width(["." * max_cell_width] * len(col)) + else: + cur_row_width = self.calc_row_width(col) + + # keep track of x-coordinate, and board x-index + cur_x = self.x + ix = 0 + + # loop through all cells in the column + for cell in col: + + tile = self.tiles[cell] + + if self.uniform_width: + cell_width = self.calc_cell_width("." * max_cell_width) + else: + cell_width = self.calc_cell_width(tile.out_val) + + target_x = cur_x + if self.align == Ouija.Align.RIGHT: + target_x += max_row_width - cur_row_width + if self.align == Ouija.Align.CENTER: + target_x += math.floor(max_row_width / 2) - math.floor(cur_row_width / 2) + + + # create the format string with the appropriate width + target_width = max_width if self.uniform_width else len(tile.out_val) + fmt_string = "{:^" + str(target_width) + "}" + + # add A_STANDOUT if the cell needs to be highlighted + target_style = tile.style + if hx == ix and hy == iy: + target_style = target_style | curses.A_STANDOUT + + # draw the key + #self.draw_rect_key(cur_y, cur_x, fmt_string.format(tile.out_val), tile.color, target_style) + self.draw_rect_key(cur_y, target_x, fmt_string.format(tile.out_val), tile.color, target_style) + + # update the x-coordinate, based on width, padding, and margin + # if there's a vertical edge, add an additional position + #cur_x += target_width + (2 * self.horiz_pad) + self.horiz_margin + cur_x += cell_width - 1 + self.horiz_margin + + # update the board x-index + ix += 1 + + # update the y-coordinate, based on padding and margin + # if there's a horizontal edge, add an additional position + cur_y += 1 + (2 * self.vert_pad) + self.vert_margin + if self.horiz_edge is not None: + cur_y += 1 + + # update the board x-index + iy += 1 + + # draw a rectangular key, with the top corner at the given (y, x), + # the specified text, color, and style + def draw_rect_key(self, y, x, text, color, style): + # calculate the inner width, including padding + iw = len(text) + (2 * self.horiz_pad) + + # outer width and horizontal offset are defaults + ow = iw + h_off = 0 + v_off = 1 + + # if we have a vertical edge, adjust the outer width and horizontal offset + if self.vert_edge is not None: + ow += 2 + h_off += 1 + + # create format strings for the inner and outer widths + owf = "{:^" + str(ow) + "}" + iwf = "{:^" + str(iw) + "}" + + # calculate how tall each cell should be, + # and where in the cell the text should appear + max_y = y + 2 + (2 * self.vert_pad) + mid_y = math.ceil((max_y - y) / 2) + + # draw the horizontal edges, if defined + if self.horiz_edge is not None: + + # top edge begins at (y, x), + # is 'iw' long, + # centered within the 'ow' length + self.stdscr.addstr(y, x, owf.format(self.horiz_edge * iw)) + + # bottom edge takes into account vertical offset + # and vertical padding + self.stdscr.addstr(y + (2 * v_off) + (2 * self.vert_pad), x, owf.format(self.horiz_edge * iw)) + + # draw the vertical edges, if defined + if self.vert_edge is not None: + + # loop through each row we need to draw + for iy in range(y + 1, max_y): + self.stdscr.addstr(iy, x, self.vert_edge) + self.stdscr.addstr(iy, x + ow - 1, self.vert_edge) + + # draw the corners, if defined + if self.corner is not None: + self.stdscr.addstr(y, x, self.corner) + self.stdscr.addstr(y, x + ow - 1, self.corner) + self.stdscr.addstr(max_y, x, self.corner) + self.stdscr.addstr(max_y, x + ow - 1, self.corner) + + # draw the inside + for iy in range(y + 1, max_y): + + # if we're at the mid-point of the cell, draw the text + # otherwise, don't draw anything + target_text = text if iy == (y + mid_y) else "" + self.stdscr.addstr(iy, x + h_off, iwf.format(target_text), style | curses.color_pair(color.color_pair())) + +class Tile: + + def __init__(self, in_val, out_val, color=Ouija.COLORS["DEFAULT"], style=0): + self.in_val = in_val + self.out_val = out_val + self.color = color + self.style = style + + def __str__(self): + return "[{},{},{}]".format(self.in_val, self.out_val, str(self.color)) + +# helper function, not sure where it goes yet +def biggest_cell(board): + return max([max(list(map(lambda x: len(x), row))) for row in board]) + diff --git a/sweepy.py b/sweepy.py new file mode 100755 index 0000000..bc173f5 --- /dev/null +++ b/sweepy.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +import curses +from logic import MinesweeperLogic +from ouija import Ouija, Tile +from pathlib import Path +import os + +HOME = Path(os.environ["HOME"]) +SWEEPY_ROOT = ".sweepy" +MY_SWEEPY_ROOT = HOME / SWEEPY_ROOT +MY_SWEEPY_WON = MY_SWEEPY_ROOT / "won" +MY_SWEEPY_LOST = MY_SWEEPY_ROOT / "lost" + +def generate_tiles(): + + tiles = [] + tiles.append(Tile("0", "")) + tiles.append(Tile("1", "1", Ouija.COLORS["DEFAULT"], curses.A_BOLD)) + tiles.append(Tile("2", "2", Ouija.COLORS["BLUE"], curses.A_BOLD)) + tiles.append(Tile("3", "3", Ouija.COLORS["RED"], curses.A_BOLD)) + tiles.append(Tile("4", "4", Ouija.COLORS["GREEN"], curses.A_BOLD)) + tiles.append(Tile("5", "5", Ouija.COLORS["ORANGE"], curses.A_BOLD)) + tiles.append(Tile("!", "#", Ouija.COLORS["YELLOW"])) + tiles.append(Tile("_", ".")) + tiles.append(Tile("x", "x", Ouija.COLORS["BLACK_ON_YELLOW"])) + tiles.append(Tile("-", ".", Ouija.COLORS["BLACK_ON_YELLOW"])) + tiles.append(Tile("+", "#", Ouija.COLORS["BLACK_ON_YELLOW"])) + return tiles + +def env_setup(): + if not MY_SWEEPY_ROOT.exists(): + MY_SWEEPY_ROOT.mkdir() + if not MY_SWEEPY_WON.exists(): + MY_SWEEPY_WON.mkdir() + if not MY_SWEEPY_LOST.exists(): + MY_SWEEPY_LOST.mkdir() + +def game_loop(stdscr, ob, ml): + + cur_y, cur_x = ml.get_first_cell() + + while(True): + + if ml.has_won() or ml.has_lost(): + msg = "You won!" if ml.has_won() else "You lost :(" + base_fn = MY_SWEEPY_WON if ml.has_won() else MY_SWEEPY_LOST + + stdscr.addstr(4, 3, "{} [n]ew game, [r]eplay, or [q]uit?".format(msg)) + ob.draw_board(ml.get_board()) + + fn = "{}.sweepy".format(ml.SEED) + root_fn = MY_SWEEPY_ROOT / fn + fn_fq = MY_SWEEPY_WON / fn + if root_fn.exists(): + root_fn.unlink() + + ml.save(str(fn_fq)) + + while(True): + + ik = stdscr.getkey() + if ik == 'n': + # clear the line + stdscr.move(4, 3) + stdscr.clrtoeol() + + ml = MinesweeperLogic() + ml.new_game() + cur_y, cur_x = ml.get_first_cell() + break + + elif ik == 'r': + stdscr.move(4, 3) + stdscr.clrtoeol() + ml.new_game() + cur_y, cur_x = ml.get_first_cell() + break + + elif ik == 'q': + exit() + + game_head(stdscr, ml) + game_foot(stdscr) + ob.draw_board(ml.get_board(), cur_y, cur_x) + in_key = stdscr.getkey() + + # uncover a cell using the enter key + if in_key == "\n": + ml.do_move(cur_y, cur_x, "u") + if ml.get_first_cell(): + cur_y, cur_x = ml.get_closest_cell(cur_y, cur_x) + #cur_y, cur_x = ml.closest(cur_y, cur_x) + + # flag a cell using the "f" key + elif in_key == "f": + ml.do_move(cur_y, cur_x, "x") + if ml.get_first_cell(): + cur_y, cur_x = ml.get_closest_cell(cur_y, cur_x) + #cur_y, cur_x = ml.closest(cur_y, cur_x) + + # save a game (and exit) using the "s" key + elif in_key == "s": + ob.draw_board(ml.get_board()) + fn = "{}.sweepy".format(ml.SEED) + root_fn = MY_SWEEPY_ROOT / fn + ml.save(str(root_fn)) + stdscr.addstr(4, 3, "Game saved!") + stdscr.getch() + exit() + + # start a new game with the "n" key + elif in_key == "n": + ob.draw_board(ml.get_board()) + stdscr.addstr(4, 3, "new game? (y/n) ") + choice = stdscr.getkey() + stdscr.move(4, 3) + stdscr.clrtoeol() + + if choice == 'y': + ml = MinesweeperLogic() + ml.new_game() + cur_y, cur_x = ml.get_first_cell() + + # reset the current game with the "r" key + elif in_key == "r": + ob.draw_board(ml.get_board()) + stdscr.addstr(4, 3, "reset the current game? (y/n) ") + choice = stdscr.getkey() + stdscr.move(4, 3) + stdscr.clrtoeol() + + if choice == 'y': + ml.new_game() + cur_y, cur_x = ml.get_first_cell() + + # quit without saving using the "q" key + elif in_key == "q": + ob.draw_board(ml.get_board()) + stdscr.addstr(4, 3, "Quit without saving? (y/n) ") + choice = stdscr.getkey() + if choice == 'y': + exit() + else: + stdscr.move(4, 3) + stdscr.clrtoeol() + + # navigation + elif in_key == "KEY_LEFT": + cur_y, cur_x, cost = ml.get_left(cur_y, cur_x) + elif in_key == "KEY_RIGHT": + cur_y, cur_x, cost = ml.get_right(cur_y, cur_x) + elif in_key == "KEY_UP": + cur_y, cur_x, cost = ml.get_up(cur_y, cur_x) + elif in_key == "KEY_DOWN": + cur_y, cur_x, cost = ml.get_down(cur_y, cur_x) + elif in_key == "\t": + cur_y, cur_x = ml.closest(cur_y, cur_x) + +def game_head(stdscr, ml): + stdscr.addstr(1, 3, "~ sweepy ~") + stdscr.addstr(2, 3, "{} bombs, seed: {}".format(str(ml.BOMBS),str(ml.SEED))) + stdscr.addstr(3, 3, "flags: {:>2}".format(str(ml.get_flag_count()))) + stdscr.refresh() + +def game_foot(stdscr): + stdscr.addstr(24, 3, "u/d/l/r/tab navigation") + stdscr.addstr(25, 3, "enter: uncover cell") + stdscr.addstr(26, 3, "'f': (un)flag cell") + stdscr.addstr(27, 3, "'n': new game") + stdscr.addstr(28, 3, "'r': reset game") + stdscr.addstr(29, 3, "'s': save and quit game") + stdscr.addstr(30, 3, "'q': quit game (without saving)") + +def main(stdscr): + + stdscr.clear() + curses.curs_set(False) + + env_setup() + + # blocks are 2 tall and 4 wide + ml = MinesweeperLogic() + board = Ouija(stdscr, 6, 5) + board.style(corner=None, horiz_edge="=") + + if not board.setup_tiles(generate_tiles()): + return False + + # print some info + stdscr.addstr(1, 3, "~ sweepy ~") + stdscr.addstr(2, 3, "[n]ew game, or [r]esume a game") + + # initial loop + while(True): + in_key = stdscr.getkey() + + if in_key == "q": + exit() + elif in_key == "n": + stdscr.addstr(3, 3, "choose a size") + stdscr.addstr(4, 3, "[0] 8 x 8 (10 bombs)") + stdscr.addstr(5, 3, "[1] 16 x 8 (25 bombs)") + while True: + choice = stdscr.getkey() + if choice == '0': + ml.new_game(8, 8, 10) + break + elif choice == '1': + ml.new_game(8, 16, 25) + break + else: + stdscr.addstr(6, 3, "invalid choice, choose again") + break + elif in_key == "l": + stdscr.addstr(3, 3, "what game do you want to load? ") + fn = stdscr.getstr() + stdscr.addstr(4, 3, fn) + stdscr.getch() + ml.load(fn.decode('utf-8')) + break + elif in_key == "r": + current_games = sorted(MY_SWEEPY_ROOT.glob("*.sweepy")) + stdscr.addstr(3, 3, "there are " + str(len(current_games)) + " games in progress") + for idx, game in enumerate(current_games): + stdscr.addstr(4 + idx, 3, "[{}] {}".format(str(idx), str(game.name))) + + if len(current_games) != 0: + stdscr.addstr(4 + len(current_games), 3, "choose a game") + while True: + r_key = stdscr.getkey() + if r_key.isdigit() and int(r_key) in range(len(current_games)): + r_game = current_games[int(r_key)] + ml.load(str(r_game)) + break + else: + stdscr.addstr(4 + len(current_games), 3, "invalid choice, choose a game") + break + + stdscr.clear() + game_loop(stdscr, board, ml) + +curses.wrapper(main) |
