A few days ago someone posted a clever bubble-wrap “mini-game” on Discord.
It was posted on a private server and my spouse ask me to forward that to them, but it worked with Discord’s spoiler-tag feature and if I waned to copy-paste it, the content I get was the content without spoiler-tags.
No problem, I’m an engineer. In seconds I crafted a quick python script that’s
able to generate an NxN
bubble-wrap for Discord.
print("Anyone want some bubble wrap?")
size = 8
print("\n".join(["||pop||" * size] * size))
The first line is important because The first line is always contains the display name of the user who sent the message so it would look terrible without the initial “spam text”.
We can do more
A few minutes later someone wrote
Popping this feels like playing Minesweeper without hitting a bomb xD
And, that’s it, it was something, something interesting. What do we need?
First of all, we need math
and random
, because we want to place mines on
random positions and if we want to determine the number of mines based on a
given percentage of the grid, we have to use one of them: round
, ceil
,
floor
.
Obviously, we need a function that can wrap a string in spoiler-tags
(on Discord it’s ||hide me||
).
import math
import random
def hide(content):
return f'||{content}||'
Next thing to build is the grid itself and place some mines on in.
BOMB_MARK = 'X'
size = 8
number_of_bombs = math.ceil(size*size * 0.2)
grid = " " * size * size
bombs = set()
while len(bombs) < number_of_bombs:
bombs.add(random.randint(0, size*size - 1))
for i in bombs:
grid = grid[:i] + BOMB_MARK + grid[i+1:]
That’s it, we have a size * size
grid with 20% mines on it. We used set
so
while we generate random numbers, we can store them in the bombs
set and we
don’t have to care about duplications. It’s an easy way to generate N
random
numbers without duplications.
What next? We have to update all cells without a mine with a number that reflects how many adjacent mines it has.
So if our grid looks like this:
___
__B
B__
We want to update all cells to look like this:
_11
12B
B21
First real problem we just met, how to count them. Because we are using a single string to represent the while grid, the easiest way is to simply check all possible adjacent cells. For that (for readability) we can use a simple matrix-like array with all the possible cells relative to a given cell.
# (x, y)
search_matrix = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1), ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1)
]
Now we can simply iterate through our grid and we can calculate position with:
x2 = index + x1
y2 = index + y1 * size
for i, c in enumerate(grid):
# if it's a bomb, we can skip
if c == BOMB_MARK:
continue
mines_around = [grid[i+p+(q*size)] == BOMB_MARK for (p, q) in search_matrix]
It seems logical, but we will get a huge error when we are trying to reach indexes out of range, so we have to check if a given index is possible at all or not. We can introduce a simple function to check if it’s possible or not:
def possible(grid, i, p, q):
v = i+p+(q*size)
if v < 0 or v >= len(grid):
return False
return True
With this new function, we can fix out problem. At the same time we can convert our results to numbers:
for i, c in enumerate(grid):
# if it's a bomb, we can skip
if c == BOMB_MARK:
continue
mines_around = len(list(filter(None, [
grid[i+p+(q*size)] == BOMB_MARK
for (p, q) in search_matrix
if possible(grid, i, p, q)])))
# Update the cell if it's not 0
if mines_around > 0:
grid = grid[:i] + str(mines_around) + grid[i+1:]
It’s not obvious first, but this code still have a small bug. It counts
adjacent cells through new-line, which is not the case we want, fortunately we
can extend our possible
function with a simple logic that checks if the
calculated row is in the same row as our target row.
def possible(grid, i, p, q):
v = i+p+(q*size)
if v < 0 or v >= len(grid):
return False
return (v // size) == ((i + (q*size)) // size)
And now, we are ready to render our grid.
for i in range(0, len(grid), size):
print("".join([hide(c) for c in grid[i:i+size]]))
Characters have no fixed width, we have to find something with fixed width. What can we use? Emojis:
bomb
for a minewhite_large_square
for an empty cellone -> eight
for numbers
# This function is here only because
# Hugo has no ability to disable emoji conversion per page
# and I did not find a better solution to write ": bomb :"
# without spaces and without auto-convert it to "💣"
def wrap(s):
return f':{s}:'
def to_emoji(char):
if char == BOMB_MARK:
return wrap("bomb")
if char == " ":
return wrap("white_large_square")
numbers = [
"one", "two", "three",
"four", "five", "six",
"seven", "eight"
]
return wrap(numbers[int(char)-1])
We are ready to render the final grid, and now we add a spam-text
line too,
to be sure it’s not messed up when we post it on Discord.
print("Shall we play a game?")
for i in range(0, len(grid), size):
print("".join([hide(to_emoji(c)) for c in grid[i:i+size]]))
Full Source
import math
import random
def hide(content):
return f'||{content}||'
def possible(grid, i, p, q):
v = i+p+(q*size)
if v < 0 or v >= len(grid):
return False
return (v // size) == ((i + (q*size)) // size)
# This function is here only because
# Hugo has no ability to disable emoji conversion per page
# and I did not find a better solution to write ": bomb :"
# without spaces and without auto-convert it to "💣"
def wrap(s):
return f':{s}:'
def to_emoji(char):
if char == BOMB_MARK:
return wrap("bomb")
if char == " ":
return wrap("white_large_square")
numbers = [
"one", "two", "three",
"four", "five", "six",
"seven", "eight"
]
return wrap(numbers[int(char)-1])
BOMB_MARK = 'X'
size = 8
number_of_bombs = math.ceil(size*size * 0.2)
grid = " " * size * size
bombs = set()
while len(bombs) < number_of_bombs:
bombs.add(random.randint(0, size*size - 1))
for i in bombs:
grid = grid[:i] + BOMB_MARK + grid[i+1:]
# (x, y)
search_matrix = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1), ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1)
]
for i, c in enumerate(grid):
# if it's a bomb, we can skip
if c == BOMB_MARK:
continue
mines_around = len(list(filter(None, [
grid[i+p+(q*size)] == BOMB_MARK
for (p, q) in search_matrix
if possible(grid, i, p, q)])))
# Update the cell if it's not 0
if mines_around > 0:
grid = grid[:i] + str(mines_around) + grid[i+1:]
print("Shall we play a game?")
for i in range(0, len(grid), size):
print("".join([hide(to_emoji(c)) for c in grid[i:i+size]]))