aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md12
-rw-r--r--logic.py391
-rw-r--r--ouija.py327
-rwxr-xr-xsweepy.py243
4 files changed, 972 insertions, 1 deletions
diff --git a/README.md b/README.md
index b263910..8d0ec86 100644
--- a/README.md
+++ b/README.md
@@ -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)