5
\$\begingroup\$

I've written the following single-script minesweeper console game and would appreciate your feedback!

What I'm interested in:

  • Enums: I think the usage of Enums could be better. It feels weird that I need to make the player_field a List[List[Union[int, PlayerFieldState]]] instead of simply a List[List[PlayerFieldState]]
  • Separation of I/O and game logic: I think there is quite a bit of code which only deals with fixing user input / outputting the field to the user. Is there a nice way to separate both?
  • Anything that would make the code easier to read / more concise

What I'm not interested in:

  • Pure formatting: I use black and isort and I'm pretty happy with that.

Code

You need Python 3.6+ with numpy to execute the following:

# Core Library modules
import enum
from typing import List, Tuple, cast, Union
# Third party modules
import numpy as np
def main():
 game = Game(n=10, m=10, nb_mines=10)
 while game.is_running:
 game.take_step()
class PlayerFieldState(enum.Enum):
 ZERO = 0
 ONE = 1
 TWO = 2
 THREE = 3
 FOUR = 4
 FIVE = 5
 SIX = 6
 SEVEN = 7
 EIGHT = 8
 UNDISCOVERED = 9
 EXPLODED_BOMB = 10
 MARKED = 11
class PlayerAction(enum.Enum):
 EXPLORE = 0
 MARK = 1
field_state2str = {
 0: " ",
 1: "1 ",
 2: "2 ",
 3: "3 ",
 4: "4 ",
 5: "5 ",
 6: "6 ",
 7: "7 ",
 8: "8 ",
 PlayerFieldState.UNDISCOVERED: "? ",
 PlayerFieldState.EXPLODED_BOMB: "πŸ’₯",
 PlayerFieldState.MARKED: "πŸ“Œ",
}
class Game:
 def __init__(self, n: int, m: int, nb_mines: int):
 self.mine_field = Game.create_field(n, m, nb_mines)
 self.player_field: List[List[Union[int, PlayerFieldState]]] = [
 [PlayerFieldState.UNDISCOVERED for _ in range(m)] for _ in range(n)
 ]
 self.is_running = True
 self.nb_mines = nb_mines
 self.nb_marked = 0
 @staticmethod
 def create_field(n: int, m: int, nb_mines: int) -> List[List[bool]]:
 field = np.zeros(n * m, dtype=bool)
 for i in range(nb_mines):
 field[i] = True
 np.random.shuffle(field)
 field = field.reshape((n, m))
 return field.tolist()
 def print_field(self, uncover: bool = False):
 top_line = [" "] + [str(y) + " " for y in range(len(self.player_field[0]))]
 top_line_str = " ".join(top_line)
 print(top_line_str)
 print("-" * len(top_line_str))
 for x, line in enumerate(self.player_field):
 to_print = [str(x) + "|"]
 for y, field in enumerate(line):
 el = field_state2str[field]
 if uncover:
 if (
 self.mine_field[x][y]
 and field != PlayerFieldState.EXPLODED_BOMB
 ):
 el = "πŸ’£"
 if field == PlayerFieldState.MARKED:
 if not self.mine_field[x][y]:
 el = "πŸ˜–"
 else:
 el = "πŸ“Œ"
 to_print.append(el)
 print(" ".join(to_print))
 print("-" * len(top_line_str))
 print(top_line_str)
 def take_step(self):
 self.print_field()
 x, y, step_type = self.get_valid_input()
 if step_type == PlayerAction.MARK:
 self.take_step_mark_bomb(x, y)
 elif step_type == PlayerAction.EXPLORE:
 self.take_step_explore(x, y)
 else:
 raise ValueError(f"step_type={step_type} is not known")
 def take_step_mark_bomb(self, x: int, y: int):
 if self.player_field[x][y] == PlayerFieldState.MARKED:
 self.player_field[x][y] = PlayerFieldState.UNDISCOVERED
 self.nb_marked -= 1
 elif self.player_field[x][y] == PlayerFieldState.UNDISCOVERED:
 self.player_field[x][y] = PlayerFieldState.MARKED
 self.nb_marked += 1
 self.check_win_condition()
 def take_step_explore(self, x: int, y: int):
 if self.player_field[x][y] == PlayerFieldState.MARKED:
 print(f"Please unmark ({x},{y}) before you hit it.")
 elif self.mine_field[x][y]:
 self.player_field[x][y] = PlayerFieldState.EXPLODED_BOMB
 self.is_running = False
 print("Game over")
 self.print_field(uncover=True)
 else:
 self.uncover(x, y)
 def check_win_condition(self):
 if self.nb_marked == self.nb_mines:
 print(
 f"You have marked {self.nb_marked} fields with mines. "
 "This is equal to the number of hidden mines."
 )
 stop_game = get_yes_no("Do you want to finish the game?")
 if stop_game:
 self.is_running = False
 if self.are_mines_marked_correctly():
 print("You won!")
 else:
 print("You lose!")
 self.print_field(uncover=True)
 def uncover(self, x: int, y: int):
 adjacent_mines = self.get_adjacent_mines(x, y)
 self.player_field[x][y] = adjacent_mines
 if adjacent_mines == 0:
 for xn, yn in self.get_neighbors(x, y):
 if self.player_field[xn][yn] == PlayerFieldState.UNDISCOVERED:
 self.uncover(xn, yn)
 def get_neighbors(self, x: int, y: int) -> List[Tuple[int, int]]:
 neighbors = []
 for xd in [-1, 0, 1]:
 for yd in [-1, 0, 1]:
 if xd == 0 and yd == 0:
 continue
 if self.is_on_field(x + xd, y + yd):
 neighbors.append((x + xd, y + yd))
 return neighbors
 def get_adjacent_mines(self, x: int, y: int) -> int:
 adjacent_mines = 0
 for xn, yn in self.get_neighbors(x, y):
 adjacent_mines += self.is_mine(xn, yn)
 return adjacent_mines
 def is_on_field(self, x: int, y: int) -> bool:
 return 0 <= x < len(self.mine_field) and 0 <= y < len(self.mine_field[0])
 def is_mine(self, x: int, y: int) -> bool:
 if not self.is_on_field(x, y):
 return False
 return self.mine_field[x][y]
 def get_valid_input(self) -> Tuple[int, int, PlayerAction]:
 position = None
 while not self.is_valid_input(position):
 position = input(
 "Where would you like to hit " "(add ',m' if you want to mark a bomb)? "
 )
 position = cast(str, position)
 splitted = position.split(",")
 x = int(splitted[0])
 y = int(splitted[1])
 if len(splitted) == 3:
 if splitted[2] == "m":
 t = PlayerAction.MARK
 else:
 t = PlayerAction.EXPLORE
 else:
 t = PlayerAction.EXPLORE
 return x, y, t
 def is_valid_input(self, position):
 if position is None:
 return False
 if "," not in position:
 return False
 splitted = position.split(",")
 x = splitted[0]
 y = splitted[1]
 if len(splitted) == 3:
 t = splitted[2]
 if t not in ["m", "b"]:
 return False
 try:
 x = int(x)
 y = int(y)
 if not self.is_on_field(x, y):
 return False
 return True
 except:
 return False
 def are_mines_marked_correctly(self):
 for x, line in enumerate(self.mine_field):
 for y, is_mine in enumerate(line):
 if is_mine:
 if self.player_field[x][y] != PlayerFieldState.MARKED:
 return False
 else:
 if self.player_field[x][y] == PlayerFieldState.MARKED:
 return False
 return True
def get_yes_no(message: str) -> bool:
 value = input(message)
 while value not in ["Y", "y", "N", "n", "yes", "no", "1", "0"]:
 value = input(message + " [y/n] ")
 return value in ["Y", "y", "yes", "1"]
if __name__ == "__main__":
 main()
asked Mar 13, 2020 at 18:17
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Requirements

You need Python 3.6+

So at the least, you should add a shebang:

#!/usr/bin/env python3

with numpy

should go in a requirements.txt.

Definition order

You define main at the top. Call me old-fashioned, but I'm used to stricter languages where the referred symbols in a function need to be defined before that function. I still consider defining things in dependence order to be more legible, and suggest that main go at the bottom.

Enum

Use unique and auto:

@unique
class PlayerFieldState(Enum):
 UNDISCOVERED = auto()
 EXPLODED_BOMB = auto()
 MARKED = auto()

I think it's probably inappropriate to represent the values 0-8 in that enum; they should be stored in a different variable. It looks like you're already going in that direction? given this:

self.player_field: List[List[Union[int, PlayerFieldState]]]

In other words: why have an int-state union if your state captures ints?

Numpy

 return field.tolist()

Why the tolist call? Wouldn't leaving the field as a Numpy array be a more efficient representation?

Iterating coordinates

get_neighbors probably shouldn't return a list. Instead, make it a generator, and don't do iterative concatenation. Something like:

 for xd in [-1, 0, 1]:
 xa = x + xd
 for yd in [-1, 0, 1]:
 ya = y + yd
 if self.is_on_field(xa, ya) and not (xd == 0 and yd == 0):
 yield xa, ya

The same goes for get_adjacent_mines.

Boolean expressions

 if not self.is_on_field(x, y):
 return False
 return self.mine_field[x][y]

can be

return self.is_on_field(x, y) and self.mine_field[x][y]

Sets for validation

while value not in ["Y", "y", "N", "n", "yes", "no", "1", "0"]:
 value = input(message + " [y/n] ")
return value in ["Y", "y", "yes", "1"]

can be

yes_vals = {"Y", "y", "yes", "1"}
no_vals = {"N", "n", "no", "0"}
while value not in yes_vals|no_vals:
 value = input(message + " [y/n] ")
return value in yes_vals

Your loop structure could also be simplified. Just do a while True, do one input, check for membership and return:

yes_vals = {"Y", "y", "yes", "1"}
no_vals = {"N", "n", "no", "0"}
while True:
 value = input(message + " [y/n] ")
 if value in yes_vals|no_vals:
 return value in yes_vals
answered Mar 14, 2020 at 4:32
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.