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
|